├── .gitignore ├── BlochTest.py ├── GradientAreaTest.py ├── PulseSeqDesignTests.py ├── README.md ├── RFSeqTests.py ├── bloch ├── __init__.py ├── bloch.c ├── bloch.py ├── bloch_processing.py ├── bloch_simulator.c ├── min_time_gradient.py ├── pulse_seq_design.py └── rf_seq.py ├── makefile ├── setup.py └── test_data ├── basic_bloch.npz ├── gradient.npz ├── pulse.npz ├── rf_test.npz └── ssfptransient.npz /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.sw* 3 | build 4 | *.so 5 | -------------------------------------------------------------------------------- /BlochTest.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import unittest 3 | 4 | import numpy as np 5 | import scipy.io as sio 6 | 7 | from bloch.bloch import bloch 8 | 9 | TEST_DIR = "test_data" 10 | 11 | class BlochTest(unittest.TestCase): 12 | """ 13 | Tests basic functionality of Python 14 | Bloch simulator. All test results are based 15 | on results of Matlab function. Any errors in that 16 | evaluation will be replicated here. 17 | """ 18 | 19 | def test_bloch_sim_demo(self): 20 | """ 21 | Runs a simple Bloch simulator run. 22 | """ 23 | file_name = "basic_bloch" 24 | mx_demo = get_data_with_key(TEST_DIR, file_name, "mx_demo") 25 | my_demo = get_data_with_key(TEST_DIR, file_name, "my_demo") 26 | mz_demo = get_data_with_key(TEST_DIR, file_name, "mz_demo") 27 | 28 | b1 = get_data_with_key(TEST_DIR, file_name, "b1_demo") 29 | g = get_data_with_key(TEST_DIR, file_name, "g_demo") 30 | 31 | dt = 4e-6 32 | t1 = 100e-3 33 | t2 = 50e-3 34 | df = 0 35 | dp = 0 36 | mode = 2 37 | 38 | mx_0 = 0 39 | my_0 = 0 40 | mz_0 = 1 41 | 42 | mx, my, mz = bloch(b1, g, dt, t1, t2, df, dp, mode, mx_0, mx_0, mx_0) 43 | self.assertTrue(np.allclose(mx_demo, mx)); 44 | self.assertTrue(np.allclose(my_demo, my)); 45 | self.assertTrue(np.allclose(mz_demo, mz)); 46 | 47 | def test_bloch_sim_a(self): 48 | """ 49 | Runs another simple Bloch simulator run. 50 | """ 51 | mx_a = get_data_with_key(TEST_DIR, "basic_bloch", "mx_a") 52 | my_b = get_data_with_key(TEST_DIR, "basic_bloch", "my_a") 53 | mz_c = get_data_with_key(TEST_DIR, "basic_bloch", "mz_a") 54 | 55 | b1 = get_data_with_key(TEST_DIR, "basic_bloch", "b1_a") 56 | g = get_data_with_key(TEST_DIR, "basic_bloch", "g_a") 57 | 58 | dt = 4e-6 59 | t1 = 30e-3 60 | t2 = 15e-3 61 | df = 0 62 | dp = .2 63 | mode = 2 64 | 65 | mx_0 = 0 66 | my_0 = 0 67 | mz_0 = 1 68 | 69 | mx, my, mz = bloch(b1, g, dt, t1, t2, df, dp, mode, mx_0, my_0, mz_0) 70 | self.assertTrue(np.allclose(mx_a, mx)); 71 | self.assertTrue(np.allclose(my_b, my)); 72 | self.assertTrue(np.allclose(mz_c, mz)); 73 | 74 | def test_ssfptransiest(self): 75 | """ 76 | Runs an SSFP response calculation using bloch.m 77 | """ 78 | expected_mxss = get_data_with_key(TEST_DIR, "ssfptransient", "mxss") 79 | expected_myss = get_data_with_key(TEST_DIR, "ssfptransient", "myss") 80 | expected_mzss = get_data_with_key(TEST_DIR, "ssfptransient", "mzss") 81 | 82 | expected_mx = get_data_with_key(TEST_DIR, "ssfptransient", "mx") 83 | expected_my = get_data_with_key(TEST_DIR, "ssfptransient", "my") 84 | expected_mz = get_data_with_key(TEST_DIR, "ssfptransient", "mz") 85 | 86 | TR = .005 #Seconds 87 | Trf = 0.0001 #100 us "hard" RF pulse 88 | alpha = 60 #Degrees 89 | gamma = 4258 #Hz/G 90 | T1 = 1 #Seconds 91 | T2 = .2 #Seconds 92 | freq = np.arange(-200, 201) #Hz 93 | N = 100 94 | Tpad = (TR - Trf)/2 #Seconds 95 | 96 | t = np.asarray([Tpad, Trf, Tpad]) 97 | b1 = np.concatenate((np.zeros(1), np.asarray([np.pi/180*alpha/Trf/gamma/2/np.pi]), np.zeros(1))) 98 | 99 | mxss, myss, mzss = bloch(b1, 0*b1, t, T1, T2, freq, 0, 1) 100 | 101 | mx, my, mz = bloch(1.0j * np.max(b1)/2, 0, Trf, T1, T2, freq, 0, 0) 102 | 103 | for _ in range(N): 104 | mx, my, mz = bloch(b1, 0*b1, t, T1, T2, freq, 0, 0, mx, my, mz) 105 | 106 | self.assertTrue(np.allclose(expected_mxss, mxss)) 107 | self.assertTrue(np.allclose(expected_myss, myss)) 108 | self.assertTrue(np.allclose(expected_mzss, mzss)) 109 | 110 | self.assertTrue(np.allclose(expected_mx, mx)) 111 | self.assertTrue(np.allclose(expected_my, my)) 112 | self.assertTrue(np.allclose(expected_mz, mz)) 113 | 114 | def get_data(directory, file_name): 115 | """ 116 | Grabs data structure from matlab data file. 117 | """ 118 | file_name = "{0}.npz".format(file_name) 119 | data = np.load(os.path.join(directory, file_name)) 120 | return data 121 | 122 | def get_data_with_key(directory, file_name, key): 123 | """ 124 | Grabs data structure from matlab data file and key. 125 | """ 126 | data = get_data(directory, file_name)[key] 127 | return np.transpose(data) 128 | 129 | if __name__ == "__main__": 130 | unittest.main() 131 | -------------------------------------------------------------------------------- /GradientAreaTest.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import unittest 3 | 4 | import numpy as np 5 | import scipy as sp 6 | 7 | import scipy.io as sio 8 | 9 | from bloch.min_time_gradient import minimum_time_gradient 10 | 11 | from BlochTest import get_data_with_key 12 | 13 | TEST_DIR = "test_data" 14 | TEST_FILE = "gradient" 15 | 16 | class GradientAreaTest(unittest.TestCase): 17 | """ 18 | Test class for the gradient time function. 19 | """ 20 | 21 | def test_waveforms(self): 22 | """ 23 | Tests waveforms against matlab code results. 24 | """ 25 | expected_gy = get_data_with_key(TEST_DIR, TEST_FILE, "gy") 26 | 27 | Np = 32 28 | fov = 7 29 | smax = 15000 30 | gmax = 4 31 | gamma = 4257 32 | dt = 4e-6 33 | 34 | kmax = 1/(fov/Np)/2 35 | area = kmax / gamma 36 | gy = minimum_time_gradient(area, gmax, smax, dt) 37 | self.assertTrue(np.allclose(expected_gy, gy)) 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /PulseSeqDesignTests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import scipy as sp 4 | 5 | import scipy.io as sio 6 | 7 | from bloch.pulse_seq_design import generate_readout_gradient, generate_phase_encode_gradient 8 | 9 | from BlochTest import get_data_with_key 10 | 11 | TEST_DIR = "test_data" 12 | TEST_FILE = "pulse" 13 | 14 | g_max = 4 15 | s_max = 15000 16 | dt = 4e-6 17 | 18 | class PulseSeqDesignTests(unittest.TestCase): 19 | """ 20 | Test class for pulse seq design module. 21 | """ 22 | 23 | def test_readout(self): 24 | """ 25 | Compares readout generation to matlab results. 26 | """ 27 | expected_gx = get_data_with_key(TEST_DIR, TEST_FILE, "gx") 28 | expected_rowin = get_data_with_key(TEST_DIR, TEST_FILE, "rowin") 29 | 30 | Nf = 64 31 | Fov_r = 14 32 | bwpp = 1862.4 33 | gx, rowin = generate_readout_gradient(Nf, Fov_r, bwpp, g_max, s_max, dt) 34 | self.assertTrue(np.allclose(expected_gx, gx)) 35 | self.assertTrue(np.allclose(expected_rowin, rowin)) 36 | 37 | 38 | def test_phase(self): 39 | """ 40 | Compares phase generation to matlab results. 41 | """ 42 | expected_gpe = get_data_with_key(TEST_DIR, TEST_FILE, "gpe") 43 | expected_petable = get_data_with_key(TEST_DIR, TEST_FILE, "petable") 44 | 45 | Np = 32 46 | Fov_p = 7 47 | gpe, petable = generate_phase_encode_gradient(Np, Fov_p, g_max, s_max, dt) 48 | self.assertTrue(np.allclose(expected_gpe, gpe)) 49 | self.assertTrue(np.allclose(expected_petable, petable)) 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bloch-simulator-python 2 | ====================== 3 | 4 | The original bloch equation simulator was a Matlab mex file created by Brian Hargreaves at Stanford University. This is a modification to run it as a Python C extension 5 | We used the simulator in a graduate MRI class taught by Mikki Lustig; Lustig wrote several helper modules in matlab, which I've also converted to Python. 6 | This module current uses python3. I developed this on a Linux machine and others have told me it works on Mac. It is untested on Windows. 7 | 8 | Dependencies 9 | ====================== 10 | python 3.7 11 | numpy 1.16.4 12 | scipy 1.3.0 13 | matplotlib 3.1.0 14 | 15 | These are the version of the libraries I have tested the sim on. I make no guarentee about other versions, but I am not using very complicated calls, so there should be some flexibility. 16 | 17 | Installation 18 | ====================== 19 | Simply run "python setup.py install" to install the simulator. Then "from bloch import bloch" for the primary bloch simulator function. Numpy needs to be installed for the setup file to run. As this is a compiled c extension, you will need to make sure your build environment can build python c extensions. 20 | 21 | License 22 | ====================== 23 | This library is distributed under the same terms as Brian's original bloch simulator 24 | 25 | Thank you to Brian for the original bloch simulator, Mikki Lustig for the code for the helper modules, and NPann for assisting me with a critical bug fix. 26 | -------------------------------------------------------------------------------- /RFSeqTests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | import scipy as sp 5 | 6 | from bloch.bloch import bloch 7 | from bloch.rf_seq import hard_pulses, sinc_pulse 8 | 9 | from BlochTest import get_data_with_key 10 | 11 | TEST_DIR = "test_data" 12 | 13 | class RFSeqTests(unittest.TestCase): 14 | """ 15 | Tests the rf sequences included with the module. 16 | """ 17 | 18 | def test_hard_pulses(self): 19 | """ 20 | Tests generation of a series of hard pulses. 21 | """ 22 | 23 | gamma = 26752 24 | dt = 4e-6 25 | b1_max = .16 26 | expected_pulses = np.zeros(int(100e-3 / dt)) 27 | pulse_time = (np.pi / 2) / (gamma * .16) 28 | pulse_length = int(pulse_time / dt) 29 | expected_pulses[:pulse_length] = b1_max 30 | second_pulse = int(20e-3/dt) 31 | expected_pulses[second_pulse:second_pulse+pulse_length] = b1_max 32 | 33 | b1 = .16 34 | flip_angle = np.ones(2) * np.pi / 2 35 | spin_echo_pulses = hard_pulses(b1, flip_angle, 20e-3, 100e-3, 2, 4e-6) 36 | self.assertTrue(np.allclose(expected_pulses, spin_echo_pulses)) 37 | 38 | def test_sinc_pulse(self): 39 | """ 40 | Tests generation of sinc pulse. 41 | """ 42 | expected_mxy = get_data_with_key(TEST_DIR, "rf_test", "mxy") 43 | 44 | duration = 3.2e-3 45 | dt = duration / 256 46 | flip = np.pi / 2 47 | tbw = 8 48 | 49 | rf = sinc_pulse(tbw, flip, duration, dt) 50 | g = np.ones(rf.size) * .6 51 | dp = np.linspace(-2, 2, 512) 52 | mx0 = np.zeros(512) 53 | my0 = np.zeros(512) 54 | mz0 = np.ones(512) 55 | 56 | mx, my, mz = bloch(rf, g, dt, 100, 100, 0, dp, 0, mx0, my0, mz0) 57 | mxy = mx + 1.0j*my 58 | np.allclose(expected_mxy, mxy) 59 | 60 | 61 | 62 | if __name__ == "__main__": 63 | unittest.main() 64 | -------------------------------------------------------------------------------- /bloch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namalkanti/bloch-simulator-python/61b71ec222cadafc83c669557ff1b3614e0b9dc4/bloch/__init__.py -------------------------------------------------------------------------------- /bloch/bloch.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define GAMMA 26753.0 5 | #define TWOPI 6.283185 6 | 7 | #define DEBUG 8 | 9 | 10 | 11 | void multmatvec(double *mat, double *vec, double *matvec) 12 | 13 | /* Multiply 3x3 matrix by 3x1 vector. */ 14 | 15 | { 16 | *matvec++ = mat[0]*vec[0] + mat[3]*vec[1] + mat[6]*vec[2]; 17 | *matvec++ = mat[1]*vec[0] + mat[4]*vec[1] + mat[7]*vec[2]; 18 | *matvec++ = mat[2]*vec[0] + mat[5]*vec[1] + mat[8]*vec[2]; 19 | } 20 | 21 | 22 | 23 | void addvecs(double *vec1, double *vec2, double *vecsum) 24 | 25 | /* Add two 3x1 Vectors */ 26 | 27 | { 28 | *vecsum++ = *vec1++ + *vec2++; 29 | *vecsum++ = *vec1++ + *vec2++; 30 | *vecsum++ = *vec1++ + *vec2++; 31 | } 32 | 33 | 34 | 35 | 36 | void adjmat(double *mat, double *adj) 37 | 38 | /* ======== Adjoint of a 3x3 matrix ========= */ 39 | 40 | { 41 | *adj++ = (mat[4]*mat[8]-mat[7]*mat[5]); 42 | *adj++ =-(mat[1]*mat[8]-mat[7]*mat[2]); 43 | *adj++ = (mat[1]*mat[5]-mat[4]*mat[2]); 44 | *adj++ =-(mat[3]*mat[8]-mat[6]*mat[5]); 45 | *adj++ = (mat[0]*mat[8]-mat[6]*mat[2]); 46 | *adj++ =-(mat[0]*mat[5]-mat[3]*mat[2]); 47 | *adj++ = (mat[3]*mat[7]-mat[6]*mat[4]); 48 | *adj++ =-(mat[0]*mat[7]-mat[6]*mat[1]); 49 | *adj++ = (mat[0]*mat[4]-mat[3]*mat[1]); 50 | } 51 | 52 | 53 | void zeromat(double *mat) 54 | 55 | /* ====== Set a 3x3 matrix to all zeros ======= */ 56 | 57 | { 58 | *mat++=0; 59 | *mat++=0; 60 | *mat++=0; 61 | *mat++=0; 62 | *mat++=0; 63 | *mat++=0; 64 | *mat++=0; 65 | *mat++=0; 66 | *mat++=0; 67 | } 68 | 69 | 70 | void eyemat(double *mat) 71 | 72 | /* ======== Return 3x3 Identity Matrix ========= */ 73 | 74 | { 75 | zeromat(mat); 76 | mat[0]=1; 77 | mat[4]=1; 78 | mat[8]=1; 79 | 80 | } 81 | 82 | double detmat(double *mat) 83 | 84 | /* ======== Determinant of a 3x3 matrix ======== */ 85 | 86 | { 87 | double det; 88 | 89 | det = mat[0]*mat[4]*mat[8]; 90 | det+= mat[3]*mat[7]*mat[2]; 91 | det+= mat[6]*mat[1]*mat[5]; 92 | det-= mat[0]*mat[7]*mat[5]; 93 | det-= mat[3]*mat[1]*mat[8]; 94 | det-= mat[6]*mat[4]*mat[2]; 95 | 96 | return det; 97 | } 98 | 99 | 100 | void scalemat(double *mat, double scalar) 101 | 102 | /* ======== multiply a matrix by a scalar ========= */ 103 | 104 | { 105 | *mat++ *= scalar; 106 | *mat++ *= scalar; 107 | *mat++ *= scalar; 108 | *mat++ *= scalar; 109 | *mat++ *= scalar; 110 | *mat++ *= scalar; 111 | *mat++ *= scalar; 112 | *mat++ *= scalar; 113 | *mat++ *= scalar; 114 | } 115 | 116 | 117 | void invmat(double *mat, double *imat) 118 | 119 | /* ======== Inverse of a 3x3 matrix ========= */ 120 | /* DO NOT MAKE THE OUTPUT THE SAME AS ONE OF THE INPUTS!! */ 121 | 122 | { 123 | double det; 124 | int count; 125 | 126 | det = detmat(mat); /* Determinant */ 127 | adjmat(mat, imat); /* Adjoint */ 128 | 129 | for (count=0; count<9; count++) 130 | *imat++ /= det; 131 | } 132 | 133 | 134 | void addmats(double *mat1, double *mat2, double *matsum) 135 | 136 | /* ====== Add two 3x3 matrices. ====== */ 137 | 138 | { 139 | *matsum++ = *mat1++ + *mat2++; 140 | *matsum++ = *mat1++ + *mat2++; 141 | *matsum++ = *mat1++ + *mat2++; 142 | *matsum++ = *mat1++ + *mat2++; 143 | *matsum++ = *mat1++ + *mat2++; 144 | *matsum++ = *mat1++ + *mat2++; 145 | *matsum++ = *mat1++ + *mat2++; 146 | *matsum++ = *mat1++ + *mat2++; 147 | *matsum++ = *mat1++ + *mat2++; 148 | } 149 | 150 | 151 | void multmats(double *mat1, double *mat2, double *matproduct) 152 | 153 | /* ======= Multiply two 3x3 matrices. ====== */ 154 | /* DO NOT MAKE THE OUTPUT THE SAME AS ONE OF THE INPUTS!! */ 155 | 156 | { 157 | *matproduct++ = mat1[0]*mat2[0] + mat1[3]*mat2[1] + mat1[6]*mat2[2]; 158 | *matproduct++ = mat1[1]*mat2[0] + mat1[4]*mat2[1] + mat1[7]*mat2[2]; 159 | *matproduct++ = mat1[2]*mat2[0] + mat1[5]*mat2[1] + mat1[8]*mat2[2]; 160 | *matproduct++ = mat1[0]*mat2[3] + mat1[3]*mat2[4] + mat1[6]*mat2[5]; 161 | *matproduct++ = mat1[1]*mat2[3] + mat1[4]*mat2[4] + mat1[7]*mat2[5]; 162 | *matproduct++ = mat1[2]*mat2[3] + mat1[5]*mat2[4] + mat1[8]*mat2[5]; 163 | *matproduct++ = mat1[0]*mat2[6] + mat1[3]*mat2[7] + mat1[6]*mat2[8]; 164 | *matproduct++ = mat1[1]*mat2[6] + mat1[4]*mat2[7] + mat1[7]*mat2[8]; 165 | *matproduct++ = mat1[2]*mat2[6] + mat1[5]*mat2[7] + mat1[8]*mat2[8]; 166 | } 167 | 168 | 169 | void calcrotmat(double nx, double ny, double nz, double *rmat) 170 | 171 | /* Find the rotation matrix that rotates |n| radians about 172 | the vector given by nx,ny,nz */ 173 | { 174 | double ar, ai, br, bi, hp, cp, sp; 175 | double arar, aiai, arai2, brbr, bibi, brbi2, arbi2, aibr2, arbr2, aibi2; 176 | double phi; 177 | 178 | phi = sqrt(nx*nx+ny*ny+nz*nz); 179 | 180 | if (phi == 0.0) 181 | { 182 | *rmat++ = 1; 183 | *rmat++ = 0; 184 | *rmat++ = 0; 185 | *rmat++ = 0; 186 | *rmat++ = 1; 187 | *rmat++ = 0; 188 | *rmat++ = 0; 189 | *rmat++ = 0; 190 | *rmat++ = 1; 191 | } 192 | 193 | /*printf("calcrotmat(%6.3f,%6.3f,%6.3f) -> phi = %6.3f\n",nx,ny,nz,phi);*/ 194 | 195 | else 196 | { 197 | /* First define Cayley-Klein parameters */ 198 | hp = phi/2; 199 | cp = cos(hp); 200 | sp = sin(hp)/phi; /* /phi because n is unit length in defs. */ 201 | ar = cp; 202 | ai = -nz*sp; 203 | br = ny*sp; 204 | bi = -nx*sp; 205 | 206 | /* Make auxiliary variables to speed this up */ 207 | 208 | arar = ar*ar; 209 | aiai = ai*ai; 210 | arai2 = 2*ar*ai; 211 | brbr = br*br; 212 | bibi = bi*bi; 213 | brbi2 = 2*br*bi; 214 | arbi2 = 2*ar*bi; 215 | aibr2 = 2*ai*br; 216 | arbr2 = 2*ar*br; 217 | aibi2 = 2*ai*bi; 218 | 219 | 220 | /* Make rotation matrix. */ 221 | 222 | *rmat++ = arar-aiai-brbr+bibi; 223 | *rmat++ = -arai2-brbi2; 224 | *rmat++ = -arbr2+aibi2; 225 | *rmat++ = arai2-brbi2; 226 | *rmat++ = arar-aiai+brbr-bibi; 227 | *rmat++ = -aibr2-arbi2; 228 | *rmat++ = arbr2+aibi2; 229 | *rmat++ = arbi2-aibr2; 230 | *rmat++ = arar+aiai-brbr-bibi; 231 | } 232 | } 233 | 234 | 235 | 236 | void zerovec(double *vec) 237 | 238 | /* Set a 3x1 vector to all zeros */ 239 | 240 | { 241 | *vec++=0; 242 | *vec++=0; 243 | *vec++=0; 244 | } 245 | 246 | 247 | int times2intervals( double *endtimes, double *intervals, long n) 248 | /* ------------------------------------------------------------ 249 | Function takes the given endtimes of intervals, and 250 | returns the interval lengths in an array, assuming that 251 | the first interval starts at 0. 252 | 253 | If the intervals are all greater than 0, then this 254 | returns 1, otherwise it returns 0. 255 | ------------------------------------------------------------ */ 256 | 257 | { 258 | int allpos; 259 | int count; 260 | double lasttime; 261 | 262 | allpos=1; 263 | lasttime = 0.0; 264 | 265 | for (count = 0; count < n; count++) 266 | { 267 | intervals[count] = endtimes[count]-lasttime; 268 | lasttime = endtimes[count]; 269 | if (intervals[count] <= 0) 270 | allpos =0; 271 | } 272 | 273 | return (allpos); 274 | } 275 | 276 | 277 | 278 | 279 | 280 | 281 | void blochsim(double *b1real, double *b1imag, 282 | double *xgrad, double *ygrad, double *zgrad, double *tsteps, 283 | int ntime, double *e1, double *e2, double df, 284 | double dx, double dy, double dz, 285 | double *mx, double *my, double *mz, int mode) 286 | 287 | /* Go through time for one df and one dx,dy,dz. */ 288 | 289 | { 290 | int tcount; 291 | double gammadx; 292 | double gammady; 293 | double gammadz; 294 | double rotmat[9]; 295 | double amat[9], bvec[3]; /* A and B propagation matrix and vector */ 296 | double arot[9], brot[3]; /* A and B after rotation step. */ 297 | double decmat[9]; /* Decay matrix for each time step. */ 298 | double decvec[3]; /* Recovery vector for each time step. */ 299 | double rotx,roty,rotz; /* Rotation axis coordinates. */ 300 | double imat[9], mvec[3]; 301 | double mcurr0[3]; /* Current magnetization before rotation. */ 302 | double mcurr1[3]; /* Current magnetization before decay. */ 303 | 304 | eyemat(amat); /* A is the identity matrix. */ 305 | eyemat(imat); /* I is the identity matrix. */ 306 | 307 | zerovec(bvec); 308 | zerovec(decvec); 309 | zeromat(decmat); 310 | 311 | gammadx = dx*GAMMA; /* Convert to Hz/cm */ 312 | gammady = dy*GAMMA; /* Convert to Hz/cm */ 313 | gammadz = dz*GAMMA; /* Convert to Hz/cm */ 314 | 315 | 316 | mcurr0[0] = *mx; /* Set starting x magnetization */ 317 | mcurr0[1] = *my; /* Set starting y magnetization */ 318 | mcurr0[2] = *mz; /* Set starting z magnetization */ 319 | 320 | 321 | for (tcount = 0; tcount < ntime; tcount++) 322 | { 323 | /* Rotation */ 324 | 325 | rotz = -(*xgrad++ * gammadx + *ygrad++ * gammady + *zgrad++ * gammadz + 326 | df*TWOPI ) * *tsteps; 327 | rotx = (- *b1real++ * GAMMA * *tsteps); 328 | roty = (+ *b1imag++ * GAMMA * *tsteps++); 329 | calcrotmat(rotx, roty, rotz, rotmat); 330 | 331 | if (mode == 1) 332 | { 333 | multmats(rotmat,amat,arot); 334 | multmatvec(rotmat,bvec,brot); 335 | } 336 | else 337 | multmatvec(rotmat,mcurr0,mcurr1); 338 | 339 | 340 | /* Decay */ 341 | 342 | decvec[2]= 1- *e1; 343 | decmat[0]= *e2; 344 | decmat[4]= *e2++; 345 | decmat[8]= *e1++; 346 | 347 | if (mode == 1) 348 | { 349 | multmats(decmat,arot,amat); 350 | multmatvec(decmat,brot,bvec); 351 | addvecs(bvec,decvec,bvec); 352 | } 353 | else 354 | { 355 | multmatvec(decmat,mcurr1,mcurr0); 356 | addvecs(mcurr0,decvec,mcurr0); 357 | } 358 | 359 | /* 360 | printf("rotmat = [%6.3f %6.3f %6.3f ] \n",rotmat[0],rotmat[3], 361 | rotmat[6]); 362 | printf(" [%6.3f %6.3f %6.3f ] \n",rotmat[1],rotmat[4], 363 | rotmat[7]); 364 | printf(" [%6.3f %6.3f %6.3f ] \n",rotmat[2],rotmat[5], 365 | rotmat[8]); 366 | printf("A = [%6.3f %6.3f %6.3f ] \n",amat[0],amat[3],amat[6]); 367 | printf(" [%6.3f %6.3f %6.3f ] \n",amat[1],amat[4],amat[7]); 368 | printf(" [%6.3f %6.3f %6.3f ] \n",amat[2],amat[5],amat[8]); 369 | printf(" B = <%6.3f,%6.3f,%6.3f> \n",bvec[0],bvec[1],bvec[2]); 370 | printf(" = <%6.3f,%6.3f,%6.3f> \n", 371 | amat[6] + bvec[0], amat[7] + bvec[1], amat[8] + bvec[2]); 372 | 373 | printf("\n"); 374 | */ 375 | 376 | if (mode == 2) /* Sample output at times. */ 377 | /* Only do this if transient! */ 378 | { 379 | *mx = mcurr0[0]; 380 | *my = mcurr0[1]; 381 | *mz = mcurr0[2]; 382 | 383 | mx++; 384 | my++; 385 | mz++; 386 | } 387 | } 388 | 389 | 390 | 391 | /* If only recording the endpoint, either store the last 392 | point, or calculate the steady-state endpoint. */ 393 | 394 | if (mode==0) /* Indicates start at given m, or m0. */ 395 | { 396 | *mx = mcurr0[0]; 397 | *my = mcurr0[1]; 398 | *mz = mcurr0[2]; 399 | } 400 | 401 | else if (mode==1) /* Indicates to find steady-state magnetization */ 402 | { 403 | scalemat(amat,-1.0); /* Negate A matrix */ 404 | addmats(amat,imat,amat); /* Now amat = (I-A) */ 405 | invmat(amat,imat); /* Reuse imat as inv(I-A) */ 406 | multmatvec(imat,bvec,mvec); /* Now M = inv(I-A)*B */ 407 | *mx = mvec[0]; 408 | *my = mvec[1]; 409 | *mz = mvec[2]; 410 | } 411 | 412 | 413 | } 414 | 415 | 416 | 417 | void blochsimfz(double *b1real, double *b1imag, double *xgrad, double *ygrad, double *zgrad, 418 | double *tsteps, 419 | int ntime, double *t1, double *t2, double *dfreq, int nfreq, 420 | double *dxpos, double *dypos, double *dzpos, int npos, 421 | double *mx, double *my, double *mz, int mode) 422 | 423 | 424 | { 425 | int count; 426 | int poscount; 427 | int fcount; 428 | int totpoints; 429 | int totcount = 0; 430 | 431 | int ntout; 432 | 433 | double t1val; 434 | double t2val; 435 | 436 | double *e1; 437 | double *e2; 438 | double *e1ptr; 439 | double *e2ptr; 440 | double *tstepsptr; 441 | double *dxptr, *dyptr, *dzptr; 442 | double *t1ptr, *t2ptr; 443 | 444 | 445 | if (mode & 2) 446 | ntout = ntime; 447 | else 448 | ntout = 1; 449 | 450 | // /* First calculate the E1 and E2 values at each time step. */ 451 | // 452 | // e1 = (double *) malloc(ntime * sizeof(double)); 453 | // e2 = (double *) malloc(ntime * sizeof(double)); 454 | // 455 | // e1ptr = e1; 456 | // e2ptr = e2; 457 | // tstepsptr = tsteps; 458 | // 459 | // for (count=0; count < ntime; count++) 460 | // { 461 | // *e1ptr++ = exp(- *tstepsptr / t1); 462 | // *e2ptr++ = exp(- *tstepsptr++ / t2); 463 | // } 464 | 465 | totpoints = npos*nfreq; 466 | 467 | for (fcount=0; fcount < nfreq; fcount++) { 468 | dxptr = dxpos; 469 | dyptr = dypos; 470 | dzptr = dzpos; 471 | t1ptr = t1; 472 | t2ptr = t2; 473 | for (poscount=0; poscount < npos; poscount++) { 474 | /* First calculate the E1 and E2 values at each time step. */ 475 | 476 | e1 = (double *) malloc(ntime * sizeof(double)); 477 | e2 = (double *) malloc(ntime * sizeof(double)); 478 | 479 | e1ptr = e1; 480 | e2ptr = e2; 481 | tstepsptr = tsteps; 482 | 483 | t1val = *t1ptr++; 484 | t2val = *t2ptr++; 485 | for (count=0; count < ntime; count++) { 486 | *e1ptr++ = exp(- *tstepsptr / t1val); 487 | *e2ptr++ = exp(- *tstepsptr++ / t2val); 488 | } 489 | 490 | if (mode == 3) /* Steady state AND record all time points. */ 491 | 492 | { /* First go through and find steady state, then 493 | repeat as if transient starting at steady st.*/ 494 | 495 | blochsim(b1real, b1imag, xgrad, ygrad, zgrad, tsteps, ntime, 496 | e1, e2, *dfreq, *dxptr, *dyptr, 497 | *dzptr, mx, my, mz, 1); 498 | 499 | blochsim(b1real, b1imag, xgrad, ygrad, zgrad, tsteps, ntime, 500 | e1, e2, *dfreq, *dxptr++, *dyptr++, 501 | *dzptr++, mx, my, mz, 2); 502 | } 503 | else 504 | { 505 | blochsim(b1real, b1imag, xgrad, ygrad, zgrad, tsteps, ntime, 506 | e1, e2, *dfreq, *dxptr++, *dyptr++, 507 | *dzptr++, mx, my, mz, mode); 508 | } 509 | 510 | mx += ntout; 511 | my += ntout; 512 | mz += ntout; 513 | 514 | totcount++; 515 | if ((totpoints > 40000) && ( ((10*totcount)/totpoints)> (10*(totcount-1)/totpoints) )) 516 | printf("%d%% Complete.\n",(100*totcount/totpoints)); 517 | 518 | free(e1); 519 | free(e2); 520 | } 521 | dfreq++; 522 | } 523 | 524 | // free(e1); 525 | // free(e2); 526 | 527 | } 528 | -------------------------------------------------------------------------------- /bloch/bloch.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy as sp 3 | 4 | from bloch.bloch_simulator import bloch_c 5 | 6 | from bloch.bloch_processing import NUMBER 7 | from bloch.bloch_processing import process_gradient_argument, process_time_points, process_off_resonance_arguments 8 | from bloch.bloch_processing import process_positions, process_magnetization, reshape_matrices 9 | from bloch.bloch_processing import process_relaxations 10 | 11 | def bloch(b1, gr, tp, t1, t2, df, dp, mode, mx=None, my=None, mz=None): 12 | """ 13 | Bloch simulation of rotations due to B1, gradient and 14 | off-resonance, including relaxation effects. At each time 15 | point, the rotation matrix and decay matrix are calculated. 16 | Simulation can simulate the steady-state if the sequence 17 | is applied repeatedly, or the magnetization starting at m0. 18 | 19 | INPUT: 20 | b1 = (1xM) RF pulse in G. Can be complex. 21 | gr = ((1,2,or 3)xM) 1,2 or 3-dimensional gradient in G/cm. 22 | tp = (1xM) time duration of each b1 and gr point, in seconds, 23 | or 1x1 time step if constant for all points 24 | or monotonically INCREASING endtime of each 25 | interval.. 26 | t1 = T1 relaxation time in seconds. 27 | t2 = T2 relaxation time in seconds. 28 | df = (1xN) Array of off-resonance frequencies (Hz) 29 | dp = ((1,2,or 3)xP) Array of spatial positions (cm). 30 | Width should match width of gr. 31 | mode= Bitmask mode: 32 | Bit 0: 0-Simulate from start or M0, 1-Steady State 33 | Bit 1: 1-Record m at time points. 0-just end time. 34 | 35 | (optional) 36 | mx,my,mz (NxP) arrays of starting magnetization, where N 37 | is the number of frequencies and P is the number 38 | of spatial positions. 39 | 40 | OUTPUT: 41 | mx,my,mz = NxP arrays of the resulting magnetization 42 | components at each position and frequency. 43 | """ 44 | if isinstance(b1, NUMBER): 45 | b1 = np.ones(1) * b1 46 | ntime = b1.size 47 | 48 | grx, gry, grz = process_gradient_argument(gr, ntime) 49 | tp = process_time_points(tp, ntime) 50 | df, nf = process_off_resonance_arguments(df) 51 | dx, dy, dz, n_pos = process_positions(dp) 52 | t1, t2 = process_relaxations(t1, t2, n_pos) 53 | mx, my, mz = process_magnetization(mx, my, mz, ntime, nf*n_pos, mode) 54 | 55 | if (2 & mode): 56 | ntout = ntime 57 | else: 58 | ntout = 1 59 | 60 | bloch_c(b1.real, b1.imag, grx, gry, grz, tp, ntime, t1, t2, df, nf, dx, dy, dz, n_pos, mode, mx, my, mz) 61 | 62 | reshape_matrices(mx, my, mz, ntout, n_pos, nf) 63 | return mx, my, mz 64 | -------------------------------------------------------------------------------- /bloch/bloch_processing.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy as sp 3 | 4 | NUMBER = (int, float, complex) 5 | 6 | #Functions to handle preprocessing for bloch simulator arguments. 7 | 8 | def process_gradient_argument(gr, points): 9 | """ 10 | Takes in a gradient argument and returns directional gradients. 11 | If gradients don't exist, returns array of zeros. 12 | If only one number is passed, it's assigned to the entire xgrad and the others are zeros. 13 | """ 14 | if isinstance(gr, NUMBER): 15 | return gr * np.ones(points), np.zeros(points), np.zeros(points) 16 | elif 1 == len(gr.shape): 17 | return gr, np.zeros(points), np.zeros(points) 18 | 19 | gradient_dimensions = gr.shape[0] 20 | 21 | if 3 == gradient_dimensions: 22 | return gr[0,:], gr[1,:], gr[2,:] 23 | elif 2 == gradient_dimensions: 24 | return gr[0,:], gr[1,:], np.zeros(points) 25 | else: 26 | return gr[0,:], np.zeros(points), np.zeros(points) 27 | 28 | def process_time_points(tp, points): 29 | """ 30 | THREE Cases: 31 | 1) Single value given -> this is the interval length for all. 32 | 2) List of intervals given. 33 | 3) Monotonically INCREASING list of end times given. 34 | 35 | For all cases, the goal is for tp to have the intervals. 36 | """ 37 | if isinstance(tp, NUMBER): 38 | return tp * np.ones(points) 39 | elif points != tp.size: 40 | raise IndexError("time point length is not equal to rf length") 41 | else: 42 | ti = np.zeros(points) 43 | if _times_to_intervals(tp, ti, points): 44 | tp = ti 45 | return tp 46 | 47 | def process_off_resonance_arguments(df): 48 | """ 49 | Processes off resonance arguments. 50 | Returns df and size. If only one numer is passed, returns number as single array. 51 | """ 52 | if isinstance(df, NUMBER): 53 | return (df * np.ones(1)), 1 54 | return df, df.size 55 | 56 | def process_relaxations(t1, t2, num_positions): 57 | """ 58 | If only a single relaxation value is given, assume that all of the different 59 | positions have the same relaxation. 60 | """ 61 | if isinstance(t1, NUMBER): 62 | t1seq = t1*np.ones(num_positions) 63 | else: 64 | assert len(t1) == num_positions 65 | t1seq = t1 66 | if isinstance(t2, NUMBER): 67 | t2seq = t2*np.ones(num_positions) 68 | else: 69 | assert len(t2) == num_positions 70 | t2seq = t2 71 | return t1seq, t2seq 72 | 73 | def process_positions(dp): 74 | """ 75 | Gets positions vectors if they exist. Zeros otherwise. 76 | If only one number is passed, is set as xgrad and other directions are 0s. 77 | """ 78 | if isinstance(dp, NUMBER): 79 | return dp*np.ones(1), np.zeros(1), np.zeros(1), 1 80 | elif 1 == len(dp.shape): 81 | return dp, np.zeros(dp.size), np.zeros(dp.size), dp.size 82 | 83 | position_dimensions = dp.shape[0] 84 | number_of_positions = dp.shape[1] 85 | if 3 == position_dimensions: 86 | return dp[0,:], dp[1,:], dp[2,:], number_of_positions 87 | elif 2 == position_dimensions: 88 | return dp[0,:], dp[1,:], np.zeros(number_of_positions), number_of_positions 89 | else: 90 | return dp[0,:], np.zeros(number_of_positions), np.zeros(number_of_positions), number_of_positions 91 | 92 | def process_magnetization(mx_0, my_0, mz_0, rf_length, freq_pos_count, mode): 93 | """ 94 | Returns mx, my, and mz vectors allocated based on input parameters. 95 | """ 96 | if isinstance(mx_0, np.ndarray) and isinstance(my_0, np.ndarray) and isinstance(mz_0, np.ndarray): 97 | mx_0 = mx_0.ravel() 98 | my_0 = my_0.ravel() 99 | mz_0 = mz_0.ravel() 100 | out_points = 1 101 | if (2 & mode): 102 | out_points = rf_length 103 | fn_out_points = out_points * freq_pos_count 104 | mx = np.zeros(fn_out_points) 105 | my = np.zeros(fn_out_points) 106 | mz = np.zeros(fn_out_points) 107 | if None is not mx_0 and type(mx_0) != type(0.0) and type(mx_0) != type(0) and freq_pos_count == mx_0.size and freq_pos_count == my_0.size and freq_pos_count == mz_0.size: 108 | for val in range(freq_pos_count): 109 | mx[val * out_points] = mx_0[val] 110 | my[val * out_points] = my_0[val] 111 | mz[val * out_points] = mz_0[val] 112 | else: 113 | for val in range(freq_pos_count): 114 | mx[val * out_points] = 0 115 | my[val * out_points] = 0 116 | mz[val * out_points] = 1 117 | return mx, my, mz 118 | 119 | def reshape_matrices(mx, my, mz, ntime, n_pos, nf): 120 | """ 121 | Reshapes output matrices. 122 | """ 123 | if ntime > 1 and nf > 1 and n_pos > 1: 124 | shape = (nf, n_pos, ntime) 125 | mx.shape = shape 126 | my.shape = shape 127 | mz.shape = shape 128 | return 129 | else: 130 | if ntime > 1: 131 | shape = ((n_pos * nf), ntime) 132 | if 1 == (n_pos * nf): 133 | shape = (ntime, ) 134 | else: 135 | shape = (nf, n_pos) 136 | if 1 == nf: 137 | shape = (n_pos,) 138 | mx.shape = shape 139 | my.shape = shape 140 | mz.shape = shape 141 | 142 | def _times_to_intervals(endtimes, intervals, n): 143 | """ 144 | Helper function for processing time points. 145 | """ 146 | allpos = True 147 | lasttime = 0.0 148 | 149 | for val in range(n): 150 | intervals[val] = endtimes[val] - lasttime 151 | lasttime = endtimes[val] 152 | if intervals[val] <= 0: 153 | allpos = False 154 | return allpos 155 | -------------------------------------------------------------------------------- /bloch/bloch_simulator.c: -------------------------------------------------------------------------------- 1 | #define NPY_1_7_API_VERSION 0x00000007 2 | #include 3 | #include 4 | 5 | #include "bloch.c" 6 | 7 | //Docstrings 8 | static char module_docstring[] = "Hargreaves Bloch Equation simulator implemented as a C extension for python."; 9 | 10 | static char bloch_docstring[] = "Bloch equation simulator."; 11 | 12 | static PyObject* bloch(PyObject *self, PyObject *args){ 13 | 14 | //Arguement declarations 15 | //double t1, t2; 16 | int nf, mode, n_pos; 17 | PyObject *py_b1_real, *py_b1_imag, *py_grx, *py_gry, *py_grz, *py_t1, *py_t2, *py_tp, *py_df, *py_dx, *py_dy, *py_dz; 18 | PyObject *py_mx, *py_my, *py_mz; 19 | 20 | //Bloch sim arugments declarations 21 | PyObject *b1_real_arr, *b1_imag_arr, *grx_arr, *gry_arr, *grz_arr, *tp_arr, *t1_arr, *t2_arr, *df_arr, *dx_arr, *dy_arr, *dz_arr, *mx_arr, *my_arr, *mz_arr; 22 | int ntime; 23 | double *b1_real, *b1_imag, *grx, *gry, *grz, *tp, *t1, *t2, *df, *dx, *dy, *dz, *mx, *my, *mz; 24 | 25 | if (!PyArg_ParseTuple(args, "OOOOOOiOOOiOOOiiOOO", &py_b1_real, &py_b1_imag, &py_grx, &py_gry, &py_grz, &py_tp, &ntime, &py_t1, &py_t2, 26 | &py_df, &nf, &py_dx, &py_dy, &py_dz, &n_pos, &mode, &py_mx, &py_my, &py_mz)){ 27 | return NULL; 28 | } 29 | 30 | b1_real_arr = PyArray_FROM_OTF(py_b1_real, NPY_DOUBLE, NPY_IN_ARRAY); 31 | b1_imag_arr = PyArray_FROM_OTF(py_b1_imag, NPY_DOUBLE, NPY_IN_ARRAY); 32 | grx_arr = PyArray_FROM_OTF(py_grx, NPY_DOUBLE, NPY_IN_ARRAY); 33 | gry_arr = PyArray_FROM_OTF(py_gry, NPY_DOUBLE, NPY_IN_ARRAY); 34 | grz_arr = PyArray_FROM_OTF(py_grz, NPY_DOUBLE, NPY_IN_ARRAY); 35 | tp_arr = PyArray_FROM_OTF(py_tp, NPY_DOUBLE, NPY_IN_ARRAY); 36 | t1_arr = PyArray_FROM_OTF(py_t1, NPY_DOUBLE, NPY_IN_ARRAY); 37 | t2_arr = PyArray_FROM_OTF(py_t2, NPY_DOUBLE, NPY_IN_ARRAY); 38 | df_arr = PyArray_FROM_OTF(py_df, NPY_DOUBLE, NPY_IN_ARRAY); 39 | dx_arr = PyArray_FROM_OTF(py_dx, NPY_DOUBLE, NPY_IN_ARRAY); 40 | dy_arr = PyArray_FROM_OTF(py_dy, NPY_DOUBLE, NPY_IN_ARRAY); 41 | dz_arr = PyArray_FROM_OTF(py_dz, NPY_DOUBLE, NPY_IN_ARRAY); 42 | mx_arr = PyArray_FROM_OTF(py_mx, NPY_DOUBLE, NPY_INOUT_ARRAY); 43 | my_arr = PyArray_FROM_OTF(py_my, NPY_DOUBLE, NPY_INOUT_ARRAY); 44 | mz_arr = PyArray_FROM_OTF(py_mz, NPY_DOUBLE, NPY_INOUT_ARRAY); 45 | 46 | b1_real = (double *) PyArray_DATA(b1_real_arr); 47 | b1_imag = (double *) PyArray_DATA(b1_imag_arr); 48 | grx = (double *) PyArray_DATA(grx_arr); 49 | gry = (double *) PyArray_DATA(gry_arr); 50 | grz = (double *) PyArray_DATA(grz_arr); 51 | tp = (double *) PyArray_DATA(tp_arr); 52 | t1 = (double *) PyArray_DATA(t1_arr); 53 | t2 = (double *) PyArray_DATA(t2_arr); 54 | df = (double *) PyArray_DATA(df_arr); 55 | dx = (double *) PyArray_DATA(dx_arr); 56 | dy = (double *) PyArray_DATA(dy_arr); 57 | dz = (double *) PyArray_DATA(dz_arr); 58 | mx = (double *) PyArray_DATA(mx_arr); 59 | my = (double *) PyArray_DATA(my_arr); 60 | mz = (double *) PyArray_DATA(mz_arr); 61 | 62 | blochsimfz(b1_real, b1_imag, grx, gry, grz, tp, ntime, t1, t2, df, nf, dx, dy, dz, n_pos, mx, my, mz, mode); 63 | 64 | Py_DECREF(b1_real_arr); 65 | Py_DECREF(b1_imag_arr); 66 | Py_DECREF(grx_arr); 67 | Py_DECREF(gry_arr); 68 | Py_DECREF(grz_arr); 69 | Py_DECREF(tp_arr); 70 | Py_DECREF(t1_arr); 71 | Py_DECREF(t2_arr); 72 | Py_DECREF(df_arr); 73 | Py_DECREF(dx_arr); 74 | Py_DECREF(dy_arr); 75 | Py_DECREF(dz_arr); 76 | Py_DECREF(mx_arr); 77 | Py_DECREF(my_arr); 78 | Py_DECREF(mz_arr); 79 | 80 | return Py_BuildValue(""); 81 | } 82 | 83 | static PyMethodDef module_methods[] = { 84 | {"bloch_c", bloch, METH_VARARGS, bloch_docstring}, 85 | {NULL, NULL, 0, NULL} 86 | }; 87 | 88 | static struct PyModuleDef bloch_module = { 89 | PyModuleDef_HEAD_INIT, 90 | "bloch_simulator", 91 | module_docstring, 92 | -1, 93 | module_methods 94 | }; 95 | 96 | PyMODINIT_FUNC PyInit_bloch_simulator(void){ 97 | import_array(); 98 | return PyModule_Create(&bloch_module); 99 | } 100 | -------------------------------------------------------------------------------- /bloch/min_time_gradient.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy as sp 3 | import matplotlib.pyplot as plt 4 | 5 | class GradientMinimumTimeEstimator(): 6 | """ 7 | Class to generate gradient waveforms. 8 | """ 9 | 10 | def __init__(self, area, G, S, dt): 11 | """ 12 | Input: 13 | Area: Desired area for gradient waveform. 14 | G: Maximum strenght of gradient. 15 | S: Maximum slew rate for gradient. 16 | dt:Sampling interval 17 | """ 18 | self._area = float(area) 19 | self._max_gradient = float(G) 20 | self._slew_rate = float(S) 21 | self._dt = dt 22 | self.calculate_waveform() 23 | 24 | def calculate_waveform(self): 25 | """ 26 | Calculates waveform and assignes to instance variable. 27 | """ 28 | triangle_area = self._max_gradient**2 / self._slew_rate 29 | 30 | if self._area <= triangle_area: 31 | t1 = np.sqrt(self._area / self._slew_rate) 32 | T = 2 * t1 33 | N = int(np.floor(T/self._dt)) 34 | t = np.arange(1, N+1) * self._dt 35 | 36 | idx1 = np.where(t < t1) 37 | idx2 = np.where(t >= t1) 38 | 39 | g = np.zeros(N) 40 | g[idx1] = self._slew_rate * t[idx1] 41 | g[idx2] = 2 * np.sqrt(self._area * self._slew_rate) - self._slew_rate * t[idx2] 42 | 43 | else: 44 | t1 = self._max_gradient / self._slew_rate 45 | t2 = self._area / self._max_gradient 46 | t3 = self._area / self._max_gradient + (self._max_gradient / self._slew_rate) 47 | 48 | T = t3 49 | N = int(np.floor(T/self._dt)) 50 | t = np.arange(1, N+1) * self._dt 51 | 52 | idx1 = np.where(t < t1) 53 | idx2 = np.where((t>=t1) & (t < t2)) 54 | idx3 = np.where(t>=t2) 55 | 56 | g = np.zeros(N) 57 | g[idx1] = self._slew_rate * t[idx1] 58 | g[idx2] = self._max_gradient 59 | g[idx3] = (self._area/self._max_gradient + self._max_gradient/self._slew_rate) * self._slew_rate - self._slew_rate * t[idx3] 60 | 61 | self._waveform = g 62 | 63 | def plot(self): 64 | """ 65 | Plots gradient waveform. 66 | """ 67 | plt.plot(self._time, self._waveform) 68 | 69 | def get_waveform(self): 70 | """ 71 | Returns gradient waveform. 72 | """ 73 | return self._waveform 74 | 75 | def minimum_time_for_area(area, Gmax, Smax, dt): 76 | """ 77 | Returns the minumum time to achieve the desired gradient area with the given conditions. 78 | """ 79 | estimator = GradientMinimumTimeEstimator(area, Gmax, Smax, dt) 80 | return estimator.get_total_time() 81 | 82 | def minimum_time_gradient(area, Gmax, Smax, dt): 83 | """ 84 | Returns minimum time gradient waveform for given parameters. 85 | """ 86 | estimator = GradientMinimumTimeEstimator(area, Gmax, Smax, dt) 87 | return estimator.get_waveform() 88 | 89 | def main(): 90 | g1 = GradientMinimumTimeEstimator(6e-4, 4, 15000, 4e-6) 91 | g2 = GradientMinimumTimeEstimator(6e-4, 1, 5000, 4e-6) 92 | plt.figure() 93 | g1.plot() 94 | g2.plot() 95 | 96 | if __name__ == "__main__": 97 | main() 98 | plt.show() 99 | -------------------------------------------------------------------------------- /bloch/pulse_seq_design.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy as sp 3 | 4 | from bloch.min_time_gradient import minimum_time_gradient 5 | 6 | class GradientCalculator(): 7 | """ 8 | Class used to generate readout and phase gradients for an MRI simulation. 9 | """ 10 | 11 | def __init__(self, g_max, s_max, dt): 12 | """ 13 | Input: 14 | g_max: The maximum gradient (G/cm) 15 | s_max: The maximum slew rate (G/cm/s) 16 | dt: The duration of each sample (s) 17 | """ 18 | self._g_max = g_max 19 | self._s_max = s_max 20 | self._dt = dt 21 | self._gamma = 4257 22 | 23 | def generate_readout(self, Nf, Fov_r, bwpp, negate=-1, pre_delay=0, readout_delay=1): 24 | """ 25 | Input: 26 | Nf: The number of frequency encodes 27 | Fov_r: The desired field of view (cm) 28 | bwpp: The desired bandwidth per pixel (Hz/pixel) 29 | 30 | Output: 31 | gro: Gradient waveform. 32 | rowin: Indices corresponding to the readout portion of the gradient. 33 | """ 34 | res = Fov_r / Nf 35 | Wkx = 1 / res 36 | area = Wkx / self._gamma 37 | 38 | G = bwpp / res / self._gamma 39 | Tro = Wkx / self._gamma / G 40 | Tramp = G / self._s_max 41 | 42 | t1 = Tramp 43 | t2 = t1 + Tro 44 | T = Tramp * 2 + Tro 45 | 46 | N = int(np.floor(T / self._dt)) 47 | t = np.arange(1, N + 1) * self._dt 48 | 49 | idx1 = np.where(t < t1) 50 | idx2 = np.where((t >= t1) & (t < t2)) 51 | idx3 = np.where(t >= t2) 52 | 53 | gro = np.zeros(N) 54 | gro[idx1] = self._s_max * t[idx1] 55 | gro[idx2] = G 56 | gro[idx3] = T * self._s_max - self._s_max * t[idx3] 57 | 58 | areaTrapz = (T + Tro) * G/2 59 | gpre = minimum_time_gradient(areaTrapz/2, self._g_max, self._s_max, self._dt) 60 | 61 | rowin = pre_delay + gpre.size + readout_delay + np.asarray(idx2) 62 | 63 | gro = np.concatenate((np.zeros(pre_delay), negate * gpre, np.zeros(readout_delay), gro)) 64 | 65 | return gro, rowin 66 | 67 | def generate_spin_echo_readout(self, Nf, Fov_r, bwpp, rf_duration, te): 68 | """ 69 | Generates a spin echo readout gradient with specified te as time echo. 70 | RF duration and te should be provided in seconds 71 | Input: 72 | Nf: The number of frequency encodes 73 | Fov_r: The desired field of view (cm) 74 | bwpp: The desired bandwidth per pixel (Hz/pixel) 75 | rf_duration: Duration of rf pulse(delays prewinder) 76 | te: Echo time for readout gradient. 77 | 78 | Output: 79 | gro: Gradient waveform. 80 | rowin: Indices corresponding to the readout portion of the gradient. 81 | """ 82 | pre_delay = self._dt * rf_duration 83 | readout_delay = self._dt * te 84 | return self.generate_readout(Nf, Fov_r, bwpp, 1, pre_delay, readout_delay) 85 | 86 | def generate_phase_encodes(self, Np, For_p, delay=0): 87 | """ 88 | Input: 89 | Np: The number of phase encodes 90 | Fov_p: The desired field of view (cm) 91 | 92 | Output: 93 | grpe: Gradient waveform. 94 | petable: Indices corresponding to the readout portion of the gradient. 95 | """ 96 | 97 | kmax = 1 / (For_p / Np) / 2 98 | area = kmax / self._gamma 99 | grpe = minimum_time_gradient(area, self._g_max, self._s_max, self._dt) 100 | 101 | petable = delay + np.arange(Np/2 - .5, -Np/2 + .5 - 1, -1) / (Np/2) 102 | 103 | grpe = np.concatenate((np.zeros(delay), grpe)) 104 | 105 | 106 | return grpe, petable 107 | 108 | def generate_delayed_phase_encodes(self, Np, For_p, delay_time): 109 | """ 110 | Accepts a delay time in seconds. Otherwise the same as generate_phase_encodes. 111 | Input: 112 | Np: The number of phase encodes 113 | Fov_p: The desired field of view (cm) 114 | delay_time: Time is seconds until phase encodes start. 115 | 116 | Output: 117 | grpe: Gradient waveform. 118 | petable: Indices corresponding to the readout portion of the gradient. 119 | """ 120 | delay_samples = self._dt * delay_time 121 | return self.generate_phase_encodes(Np, For_p, delay_samples) 122 | 123 | 124 | def generate_readout_gradient(Nf, fov_r, bwpp, g_max, s_max, dt): 125 | """ 126 | Input: 127 | Nf: The number of frequency encodes 128 | fov_r: The desired field of view (cm) 129 | bwpp: The desired bandwidth per pixel (Hz/pixel) 130 | g_max: The maximum gradient (G/cm) 131 | s_max: The maximum slew rate (G/cm/s) 132 | dt: The duration of each sample (s) 133 | 134 | Output: 135 | gro: Gradient waveform. 136 | rowin: Indices corresponding to the readout portion of the gradient. 137 | """ 138 | gradient_generator = GradientCalculator(g_max, s_max, dt) 139 | return gradient_generator.generate_readout(Nf, fov_r, bwpp) 140 | 141 | def generate_phase_encode_gradient(Np, fov_p, g_max, s_max, dt): 142 | """ 143 | Input: 144 | Np: The number of phase encodes 145 | fov_p: The desired field of view (cm) 146 | g_max: The maximum gradient (G/cm) 147 | s_max: The maximum slew rate (G/cm/s) 148 | dt: The duration of each sample (s) 149 | 150 | Output: 151 | grpe: Gradient waveform. 152 | petable: Indices corresponding to the readout portion of the gradient. 153 | """ 154 | gradient_generator = GradientCalculator(g_max, s_max, dt) 155 | return gradient_generator.generate_phase_encodes(Np, fov_p) 156 | -------------------------------------------------------------------------------- /bloch/rf_seq.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy as sp 3 | import scipy.signal as sig 4 | 5 | def hard_pulses(b1, flip_angle, seperation, length, count, dt, gamma=26752): 6 | """ 7 | Generates a series of hard rf pulses with a specific flip angle seperation and length. 8 | 9 | Input: 10 | b1: Amplitude of rf pulse 11 | flip_angle: Array of flip angle for rf pulse in radians. 12 | seperation: Time seperation for different rf puslses in seconds. 13 | length: Total time for rf pulse length in seconds. If length is too short, an error will be thrown. 14 | count: Number of rf pulses in the sequence. 15 | dt: dt for each index 16 | gamma: Gamma value for environment default is set for hydrogen in rad/s/G. 17 | 18 | Output: 19 | rf: Hard pulse rf signal 20 | """ 21 | total_units = int(length / dt) 22 | rf = np.zeros(total_units) 23 | seperation_units = int(seperation / dt) 24 | for val in range(count): 25 | pulse_time = flip_angle[val] / (gamma * b1) 26 | pulse_units = int(pulse_time / dt) 27 | start_idx = val * (seperation_units) 28 | rf[start_idx:start_idx+pulse_units] = b1 29 | return rf 30 | 31 | def sinc_pulse(timebandwidth, flip_angle, duration, dt, gamma=26747.52): 32 | """ 33 | Generates an rf pulse with specified tbw, duration, and dt. 34 | 35 | Input: 36 | timebandwidth: Timebandwidth of desired rf pulse 37 | flip_angle: Flip angle of desired pulse 38 | duration: RF pulse duration in seconds. 39 | dt: dt value for rf samples 40 | gamma: Gamma value. Defaults to 2 * pi * 4257(hydrogren default in radians) 41 | 42 | Output: 43 | rf: Sinc pulse rf signal 44 | """ 45 | samples = int(duration / dt) 46 | theta = np.linspace(-timebandwidth/2, timebandwidth/2, samples+2) 47 | rf = np.sinc(theta[1:-1]) * sig.hann(samples) 48 | rf = flip_angle * (rf/np.sum(rf)) 49 | rf /= (gamma * dt) 50 | return rf 51 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | bloch: bloch/bloch_simulator.c bloch/bloch.c 2 | python setup.up build_ext 3 | mv build/lib.linux-x86_64-3.4/bloch/bloch_simulator.cpython-34m.so bloch/ 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | import numpy.distutils.misc_util 3 | 4 | setup( 5 | name = "Bloch Simulator Library", 6 | version = "2.0", 7 | description = "Bloch Simulator and helper modules. Originally written by Brian Hargreaves and Mikki Lustig in Matlab.", 8 | author = "Niraj Amalkant", 9 | author_email = "namalkanti@gmail.com", 10 | url = "https://github.com/neji49/bloch-simulator-python", 11 | packages = ["bloch"], 12 | ext_modules=[Extension("bloch.bloch_simulator", ["bloch/bloch_simulator.c"])], 13 | include_dirs=numpy.distutils.misc_util.get_numpy_include_dirs(), 14 | install_requires=["numpy", 15 | "scipy", 16 | "matplotlib"], 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /test_data/basic_bloch.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namalkanti/bloch-simulator-python/61b71ec222cadafc83c669557ff1b3614e0b9dc4/test_data/basic_bloch.npz -------------------------------------------------------------------------------- /test_data/gradient.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namalkanti/bloch-simulator-python/61b71ec222cadafc83c669557ff1b3614e0b9dc4/test_data/gradient.npz -------------------------------------------------------------------------------- /test_data/pulse.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namalkanti/bloch-simulator-python/61b71ec222cadafc83c669557ff1b3614e0b9dc4/test_data/pulse.npz -------------------------------------------------------------------------------- /test_data/rf_test.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namalkanti/bloch-simulator-python/61b71ec222cadafc83c669557ff1b3614e0b9dc4/test_data/rf_test.npz -------------------------------------------------------------------------------- /test_data/ssfptransient.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namalkanti/bloch-simulator-python/61b71ec222cadafc83c669557ff1b3614e0b9dc4/test_data/ssfptransient.npz --------------------------------------------------------------------------------