├── AdvancedNoteBooks
├── Constrained_Reconstruction_Demo.ipynb
└── VarNetToyExample.ipynb
├── LICENSE
├── Matlab
├── bloch_nutation_solver.m
├── bloch_solver.m
├── bloch_solver_examples.m
└── rf_simulator_example.m
├── NoteBooks
├── BasicWeightedImages.ipynb
├── Field_Generation.ipynb
├── Intro_MRI_Bloch_Solvers.ipynb
├── Intro_to_Jupyter.ipynb
├── Recon_Example.ipynb
├── Selective_RF_Excitation.ipynb
├── Simulated_Sampling.ipynb
├── Spin_Echo.ipynb
└── Spoiled_Gradient_Echo.ipynb
├── README.md
└── Simulations
├── convert_data.ipynb
├── demodulation_example.py
├── epi_distortions.py
└── spiral_distortions.py
/AdvancedNoteBooks/Constrained_Reconstruction_Demo.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "provenance": [],
7 | "collapsed_sections": [],
8 | "include_colab_link": true
9 | },
10 | "kernelspec": {
11 | "name": "python3",
12 | "display_name": "Python 3"
13 | }
14 | },
15 | "cells": [
16 | {
17 | "cell_type": "markdown",
18 | "metadata": {
19 | "id": "view-in-github",
20 | "colab_type": "text"
21 | },
22 | "source": [
23 | "
"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {
29 | "id": "WxIjqKUIfTgl"
30 | },
31 | "source": [
32 | "# MRI Constrained Reconstruction Excercise\n",
33 | "\n",
34 | "This Jupyter notebook provides some hands on experience with compressed sensing like contrained reconstruction. Each code cell can be run by clicking on the upper left corner. You can also run all by using the \"Runtime\" menu on the top menu bar. When you modify one of the reconstruction paramaters, think about what you expect the change to be.\n",
35 | "\n",
36 | "# Objectives\n",
37 | "* Reconstruct images using constrained reconstructions\n",
38 | "* Understand the tradeoffs and failure mode of constrained reconstructions\n",
39 | "* Explore different regularization methods\n",
40 | "\n",
41 | "In python you need to load libraries to use them. This first cell imports a couple of key libraries to reconstruct images."
42 | ]
43 | },
44 | {
45 | "cell_type": "code",
46 | "metadata": {
47 | "id": "lXLlC5rOoIKI"
48 | },
49 | "source": [
50 | "# This is comment, Python will ignore this line\n",
51 | "\n",
52 | "# Import libraries (load libraries which provide some functions)\n",
53 | "%matplotlib inline\n",
54 | "import numpy as np # array library\n",
55 | "import math\n",
56 | "import cmath\n",
57 | "import pickle\n",
58 | "import pywt #wavelets\n",
59 | "\n",
60 | "# For interactive plotting\n",
61 | "from ipywidgets import interact, interactive, FloatSlider, IntSlider, FloatLogSlider, Dropdown\n",
62 | "from IPython.display import clear_output, display, HTML\n",
63 | "\n",
64 | "# for plotting modified style for better visualization\n",
65 | "import matplotlib.pyplot as plt \n",
66 | "import matplotlib as mpl\n",
67 | "mpl.rcParams['lines.linewidth'] = 4\n",
68 | "mpl.rcParams['axes.titlesize'] = 24\n",
69 | "mpl.rcParams['axes.labelsize'] = 20\n",
70 | "mpl.rcParams['xtick.labelsize'] = 16\n",
71 | "mpl.rcParams['ytick.labelsize'] = 16\n",
72 | "mpl.rcParams['legend.fontsize'] = 16"
73 | ],
74 | "execution_count": null,
75 | "outputs": []
76 | },
77 | {
78 | "cell_type": "markdown",
79 | "metadata": {
80 | "id": "9EOKnavHgwOV"
81 | },
82 | "source": [
83 | "# Download Raw Data\n",
84 | "We are going to download raw data from scans collected on the scanner. This is just a nice single slice, single channel datset"
85 | ]
86 | },
87 | {
88 | "cell_type": "code",
89 | "metadata": {
90 | "id": "FcIOh7F5oK1-"
91 | },
92 | "source": [
93 | "# Get some data - Data is as an HDF5 array. This data is single channel \n",
94 | "import os\n",
95 | "if not os.path.exists(\"multicoil8n.h5\"):\n",
96 | " !wget https://www.dropbox.com/s/aihpudtdonm7dxd/multicoil8n.h5\n",
97 | "\n",
98 | "import h5py as h5\n",
99 | "with h5.File('multicoil8n.h5', 'r') as hf:\n",
100 | " kdata = np.array(hf['kspace'])\n",
101 | " smaps = np.array(hf['smaps'])"
102 | ],
103 | "execution_count": null,
104 | "outputs": []
105 | },
106 | {
107 | "cell_type": "markdown",
108 | "metadata": {
109 | "id": "c572IIN_aGes"
110 | },
111 | "source": [
112 | "# Linear Operators \n",
113 | "\n",
114 | "## Subsampling of the data\n",
115 | "* Subselecting a set of lines by changing $\\Delta k_y $ controlled by accY\n",
116 | "* Subselecting a set of lines by changing $k_{max} $ in x and y controlled by kmaxX, kmaxY\n",
117 | "* Subselecting a set of point by randomly removing a set of points, controlled by random_undersampling_fraction (1=remove none)\n",
118 | "\n",
119 | "## Forward and adjoint model\n",
120 | "* Forward: Fourier tranform $E$\n",
121 | "* Adjoint: Inverse Fourier transform $E^H$\n",
122 | "\n",
123 | "## Gradient descent\n",
124 | "For a linear system solving $Loss(x)=||Ex-d||^2_2$ the gradient is \n",
125 | "\n",
126 | ">$\\frac{\\partial{Loss}}{\\partial{x}} = E^H(Ex-d) $\n",
127 | "\n",
128 | "We descend the loss by taking steps in the (-) direction of the gradient: \n",
129 | "\n",
130 | ">$x_{n+1} = x_{n} - \\alpha E^H(Ex-d)$\n",
131 | "\n",
132 | "where $\\alpha$ is the step size which for this problem is set to $1$\n",
133 | "\n",
134 | "\n",
135 | "\n"
136 | ]
137 | },
138 | {
139 | "cell_type": "code",
140 | "metadata": {
141 | "id": "gL3QjhUxqVMS"
142 | },
143 | "source": [
144 | "def undersample_data(kspace, acc_y, kmax_x, kmax_y, random_undersampling_fraction):\n",
145 | " # Subsampling\n",
146 | " kspace_us = np.zeros_like(kspace)\n",
147 | " mask = np.zeros_like(kspace)\n",
148 | "\n",
149 | " # Regular - change delta k \n",
150 | " mask[:,:,::acc_y] = 1\n",
151 | "\n",
152 | " # Sub sections - change kmax\n",
153 | " ky,kx = np.meshgrid( np.linspace(-1,1,kspace.shape[-2]),np.linspace(-1,1,kspace.shape[-1]))\n",
154 | " mask *= np.abs(kx/kmax_x) < 1\n",
155 | " mask *= np.abs(ky/kmax_y) < 1\n",
156 | "\n",
157 | " # This ensures calls to the code are repeatable\n",
158 | " np.random.seed(0)\n",
159 | " \n",
160 | " # Random undersampling in 1D\n",
161 | " if random_undersampling_fraction < 1:\n",
162 | " # Get the k-space radius ( we will fully sample the center)\n",
163 | " abs_kx = np.abs(np.linspace(-1,1,kspace_us.shape[-2]))\n",
164 | " abs_ky = np.abs(np.linspace(-1,1,kspace_us.shape[-1]))\n",
165 | " kx,ky = np.meshgrid(abs_ky, abs_kx)\n",
166 | " kr = np.sqrt( kx**2 + ky**2)\n",
167 | "\n",
168 | " # Randomly remove a fraction of the lines\n",
169 | " mask_pe = np.random.uniform(size=kspace_us.shape)\n",
170 | " mask_pe = mask_pe < random_undersampling_fraction\n",
171 | "\n",
172 | " # Fully sample 5% of the data\n",
173 | " mask_pe = np.maximum(mask_pe, kr < 0.05)\n",
174 | "\n",
175 | " # Combine with standard undersampling\n",
176 | " mask *= mask_pe\n",
177 | " \n",
178 | " kspace_us = kspace * mask\n",
179 | "\n",
180 | " return kspace_us, mask \n",
181 | "\n",
182 | "def adjoint_fourier_transform(kspace, smaps):\n",
183 | " # This is to do a FFT shift operator so that the center of k-space is at the center of the image\n",
184 | " x,y = np.meshgrid( range(kspace.shape[-2]), range(kspace.shape[-1]))\n",
185 | " chop = (-1)**( x+y)\n",
186 | "\n",
187 | " # Fourier Transform with Window Function\n",
188 | " coil_images = chop*np.fft.ifftn(kspace*chop, axes=(-2,-1))\n",
189 | "\n",
190 | " # Coil Sum\n",
191 | " image = np.sum( coil_images*np.conj(smaps), axis=0)\n",
192 | "\n",
193 | " return image\n",
194 | "\n",
195 | "def fourier_transform(image, smaps):\n",
196 | " # This is to do a FFT shift operator so that the center of k-space is at the center of the image\n",
197 | " x,y = np.meshgrid( range(image.shape[-2]), range(image.shape[-1]))\n",
198 | " chop = (-1)**( x+y)\n",
199 | "\n",
200 | " # Fourier Transform with Window Function\n",
201 | " kspace = chop*np.fft.fftn(image*smaps*chop, axes=(-2,-1))\n",
202 | "\n",
203 | " return kspace\n",
204 | "\n",
205 | "def gradient_descent(image, mask, kspace, smaps, alpha=1.0):\n",
206 | " \n",
207 | " # Generate k-space data from Image (Ex)\n",
208 | " kspace_generated = fourier_transform(image, smaps)\n",
209 | "\n",
210 | " # Take the difference from the true data (Ex-d)\n",
211 | " diff = kspace_generated - kspace\n",
212 | "\n",
213 | " # We can only take the difference with k-space samples we actually have\n",
214 | " diff_masked = mask*diff\n",
215 | " error = np.sum(np.abs(diff_masked)**2)\n",
216 | "\n",
217 | " # Calculate the gradient by fourier transforming back\n",
218 | " gradient = adjoint_fourier_transform(diff_masked, smaps)\n",
219 | "\n",
220 | " # Update the image\n",
221 | " image = image - alpha*gradient\n",
222 | "\n",
223 | " return image, error"
224 | ],
225 | "execution_count": null,
226 | "outputs": []
227 | },
228 | {
229 | "cell_type": "markdown",
230 | "source": [
231 | "# Alternating minimization using Proximal operators\n",
232 | "We will define some proximal operators which we use to minimize functions. These proximal operators are functions that minimize:\n",
233 | "\n",
234 | ">$g(x) + \\frac{1}{2}||x-x_0||^2_2 $\n",
235 | "\n",
236 | "where $g(x)$ is the function to minimize. This keeps the solution from wandering from the initial solution. For example, minimizing $||x||_1$ without a contraint would be minimized when $x$ is zero. \n",
237 | "\n",
238 | "We will use an alternating minimization problem with two steps:\n",
239 | "\n",
240 | "* Take a gradient descent step of the linear part\n",
241 | "* Take a proximal gradient descent step\n",
242 | "\n",
243 | "Below defines some functions to do these operations"
244 | ],
245 | "metadata": {
246 | "id": "Y0tybHmwSIMu"
247 | }
248 | },
249 | {
250 | "cell_type": "code",
251 | "source": [
252 | "def l1_prox(input, lamda):\n",
253 | " # Proximal gradient of L1 (sum of absolute values) is thresholding\n",
254 | " abs_input = np.abs(input)\n",
255 | " \n",
256 | " sign = input / (abs_input + 1e-9*np.max(abs_input))\n",
257 | "\n",
258 | " mag = abs_input - lamda\n",
259 | " mag = (abs(mag) + mag) / 2\n",
260 | "\n",
261 | " return mag * sign\n",
262 | "\n",
263 | "def l2_prox(input, lamda):\n",
264 | " output = input / ( 1 + lamda)\n",
265 | " return output\n",
266 | "\n",
267 | "def iterative_sense(Mask, kspace, smaps, iterations=10):\n",
268 | " # Initial guess of zeros\n",
269 | " image = np.zeros_like(kspace)\n",
270 | "\n",
271 | " for iter in range(iterations):\n",
272 | " image = gradient_descent(image, Mask, kspace, smaps)\n",
273 | "\n",
274 | " return image\n",
275 | "\n",
276 | "def iterative_sense(Mask, kspace, smaps, iterations=10, lamda=1e-3, transform=None, proximal=None):\n",
277 | " \n",
278 | " # Initial guess of zeros\n",
279 | " image = np.zeros(kspace.shape[-2:], dtype=kspace.dtype)\n",
280 | "\n",
281 | " error_kspace = []\n",
282 | " error_constraint = []\n",
283 | "\n",
284 | " for iter in range(iterations):\n",
285 | " image, error = gradient_descent(image, Mask, kspace, smaps)\n",
286 | " error_kspace.append(error)\n",
287 | "\n",
288 | " if transform is not None:\n",
289 | " image = transform.forward(image)\n",
290 | " \n",
291 | " image = proximal(image, lamda)\n",
292 | " error_constraint.append(np.sum(np.abs(image)))\n",
293 | "\n",
294 | " if transform is not None:\n",
295 | " image = transform.backward(image)\n",
296 | " \n",
297 | " return image, error_kspace, error_constraint"
298 | ],
299 | "metadata": {
300 | "id": "7w5wl85RuctB"
301 | },
302 | "execution_count": null,
303 | "outputs": []
304 | },
305 | {
306 | "cell_type": "markdown",
307 | "source": [
308 | "# Tranforms\n",
309 | "\n",
310 | "## Wavelets\n",
311 | "This uses multi-scale wavelets to compress the image\n",
312 | "\n",
313 | "## Edge\n",
314 | "This is an aproximation of total variation. It used an undecimated wavelet tranform to make the construction similar to that in wavelets.\n"
315 | ],
316 | "metadata": {
317 | "id": "cmBVefCjd-XR"
318 | }
319 | },
320 | {
321 | "cell_type": "code",
322 | "source": [
323 | "class wavelet_transform:\n",
324 | " def __init__(self, levels=4):\n",
325 | " self.levels = levels\n",
326 | "\n",
327 | " def forward(self, x):\n",
328 | " coef= pywt.wavedec2(x, 'db4', level=self.levels)\n",
329 | "\n",
330 | " # Convert to array\n",
331 | " arr, coeff_slices, coeff_shapes = pywt.ravel_coeffs(coef)\n",
332 | " self.coeff_slices = coeff_slices\n",
333 | " self.coeff_shapes = coeff_shapes\n",
334 | "\n",
335 | " return arr\n",
336 | "\n",
337 | " def backward(self,x):\n",
338 | "\n",
339 | " # Convert array to coef\n",
340 | " coef = pywt.unravel_coeffs(x, self.coeff_slices, self.coeff_shapes, output_format='wavedec2')\n",
341 | "\n",
342 | " arr = pywt.waverec2(coef, 'db4')\n",
343 | " return arr\n",
344 | "\n",
345 | "class edge_transform:\n",
346 | " def __init__(self, levels=1):\n",
347 | " self.levels = levels\n",
348 | "\n",
349 | " def forward(self, x):\n",
350 | " coef= pywt.swt2(x, 'db1', level=self.levels, trim_approx=True)\n",
351 | "\n",
352 | " # Convert to array\n",
353 | " arr, coeff_slices, coeff_shapes = pywt.ravel_coeffs(coef)\n",
354 | " self.coeff_slices = coeff_slices\n",
355 | " self.coeff_shapes = coeff_shapes\n",
356 | "\n",
357 | " return arr\n",
358 | "\n",
359 | " def backward(self,x):\n",
360 | "\n",
361 | " # Convert array to coef\n",
362 | " coef = pywt.unravel_coeffs(x, self.coeff_slices, self.coeff_shapes, output_format='swt2')\n",
363 | "\n",
364 | " arr = pywt.iswt2(coef, 'db1')\n",
365 | " return arr\n",
366 | "\n",
367 | "\n"
368 | ],
369 | "metadata": {
370 | "id": "25ulVQnseAZQ"
371 | },
372 | "execution_count": null,
373 | "outputs": []
374 | },
375 | {
376 | "cell_type": "markdown",
377 | "source": [
378 | "# Simulation and plot"
379 | ],
380 | "metadata": {
381 | "id": "eRVv0droiJO0"
382 | }
383 | },
384 | {
385 | "cell_type": "code",
386 | "source": [
387 | "def sample_and_plot(acc_y, kmax_x, kmax_y, random_undersampling_fraction, iterations, lamda, norm, transform_name):\n",
388 | "\n",
389 | " # Grab the scan data\n",
390 | " kspace = kdata.copy()\n",
391 | " kspace /= np.max(np.abs(kspace))\n",
392 | "\n",
393 | " # Normalize the sensitivity maps (allows a step size of 1)\n",
394 | " maps = smaps.copy() / np.max(np.abs(smaps))\n",
395 | "\n",
396 | " # Subsample the data\n",
397 | " kspace_us, mask = undersample_data(kspace, acc_y=acc_y, kmax_y=kmax_y, kmax_x=kmax_x, random_undersampling_fraction=random_undersampling_fraction)\n",
398 | "\n",
399 | " # Pick a transform\n",
400 | " if transform_name == 'Wavelet':\n",
401 | " transform = wavelet_transform()\n",
402 | " elif transform_name == 'Edge':\n",
403 | " transform = edge_transform()\n",
404 | " else:\n",
405 | " transform = None\n",
406 | "\n",
407 | " # Pick a norm for the the regularization\n",
408 | " if norm==1:\n",
409 | " prox = l1_prox\n",
410 | " else:\n",
411 | " prox = l2_prox\n",
412 | "\n",
413 | " # Actual reconstruction\n",
414 | " image, error_kspace, error_constraint = iterative_sense(mask, kspace_us, maps, iterations, lamda, transform=transform, proximal=prox)\n",
415 | "\n",
416 | " # Show the subsampled data\n",
417 | " fig = plt.figure(figsize=(15,10), constrained_layout=True)\n",
418 | " gs = fig.add_gridspec(3, 6)\n",
419 | " axs = []\n",
420 | " \n",
421 | " axs.append( fig.add_subplot(gs[1,5]) )\n",
422 | " plt.imshow(np.log(1e-7+np.abs(kspace_us[0])),cmap='gray')\n",
423 | " plt.grid(False)\n",
424 | " plt.title(f'Subsampled Kspace Scan')\n",
425 | " plt.ylabel(r'$K_x$ [index]')\n",
426 | " plt.xlabel(r'$K_y$ [index]')\n",
427 | " plt.xticks([], [])\n",
428 | " plt.yticks([], [])\n",
429 | "\n",
430 | " axs.append( fig.add_subplot(gs[:,0:4]) ) \n",
431 | " plt.imshow(np.rot90(np.abs(image),-1),cmap='gray')\n",
432 | " plt.grid(False)\n",
433 | " plt.title(f'Reconstructed Image')\n",
434 | " plt.ylabel(r'$x$ [index]')\n",
435 | " plt.xlabel(r'$y$ [index]')\n",
436 | " plt.clim(0, 8*np.mean(np.abs(image)))\n",
437 | " plt.xticks([], [])\n",
438 | " plt.yticks([], [])\n",
439 | "\n",
440 | " \n",
441 | " axs.append( fig.add_subplot(gs[2,5]) ) \n",
442 | " plt.semilogy(error_kspace)\n",
443 | " plt.semilogy(error_constraint)\n",
444 | " plt.ylabel(r'error')\n",
445 | " plt.xlabel(r'iteration')\n",
446 | " plt.legend(('Error Kspace','Error Constraint'))\n",
447 | " \n",
448 | " plt.show()\n",
449 | "\n",
450 | "\n",
451 | "w = interactive(sample_and_plot, \n",
452 | " iterations=IntSlider(min=1, max=200, step=1, value=1, description='Iterations', continuous_update=False),\n",
453 | " acc_y=IntSlider(min=1, max=4, step=1, value=1, description='Stride in Y', continuous_update=False),\n",
454 | " kmax_x=FloatSlider(min=0.1, max=1, step=0.1, value=1, description='Kmax X', continuous_update=False),\n",
455 | " kmax_y=FloatSlider(min=0.1, max=1, step=0.1, value=1, description='Kmax Y', continuous_update=False),\n",
456 | " lamda=FloatLogSlider(value=-3, base=10, min=-10, max=0, description='Lamda', continuous_update=False),\n",
457 | " random_undersampling_fraction=FloatSlider(min=0.1, max=1, step=0.1, value=1, description='Rand sample', continuous_update=False),\n",
458 | " norm=IntSlider(min=1, max=2, step=1, value=1, description='Norm', continuous_update=False),\n",
459 | " transform_name=Dropdown(options=['None', 'Wavelet', 'Edge'], value='None', description='Spatial Transform:'))\n",
460 | "\n",
461 | " \n",
462 | "display(w)"
463 | ],
464 | "metadata": {
465 | "id": "zYcqPN6LqGJp"
466 | },
467 | "execution_count": null,
468 | "outputs": []
469 | },
470 | {
471 | "cell_type": "markdown",
472 | "source": [
473 | "# Code to compare sampling"
474 | ],
475 | "metadata": {
476 | "id": "uIOg2Z3NGdVX"
477 | }
478 | },
479 | {
480 | "cell_type": "code",
481 | "source": [
482 | "# Grab the scan data\n",
483 | "kspace = kdata.copy()\n",
484 | "kspace /= np.max(np.abs(kspace))\n",
485 | "\n",
486 | "# Normalize the sensitivity maps (allows a step size of 1)\n",
487 | "maps = smaps.copy() / np.max(np.abs(smaps))\n",
488 | "\n",
489 | "for pattern in range(2):\n",
490 | "\n",
491 | " # Subsample the data\n",
492 | " if pattern == 0:\n",
493 | " kspace_us, mask = undersample_data(kspace, acc_y=4, kmax_y=1, kmax_x=1, random_undersampling_fraction=1)\n",
494 | " else:\n",
495 | " kspace_us, mask = undersample_data(kspace, acc_y=1, kmax_y=1, kmax_x=1, random_undersampling_fraction=0.25)\n",
496 | "\n",
497 | " # Actual reconstructions\n",
498 | " image_std, error_kspace, error_constraint = iterative_sense(mask, kspace_us, maps, iterations=1, lamda=0, transform=edge_transform(), proximal=l1_prox)\n",
499 | " image_sense, error_kspace, error_constraint = iterative_sense(mask, kspace_us, maps, iterations=200, lamda=0, transform=edge_transform(), proximal=l1_prox)\n",
500 | " image_cs, error_kspace, error_constraint = iterative_sense(mask, kspace_us, maps, iterations=200, lamda=1e-6, transform=edge_transform(), proximal=l1_prox)\n",
501 | "\n",
502 | " plt.figure(figsize=(20,10))\n",
503 | " plt.subplot(131)\n",
504 | " plt.imshow(np.rot90(np.abs(image_std[32:-32,32:-32]),-1),cmap='gray')\n",
505 | " plt.xticks([], [])\n",
506 | " plt.yticks([], [])\n",
507 | " plt.title('Standard')\n",
508 | "\n",
509 | " plt.subplot(132)\n",
510 | " plt.imshow(np.rot90(np.abs(image_sense[32:-32,32:-32]),-1),cmap='gray')\n",
511 | " plt.xticks([], [])\n",
512 | " plt.yticks([], [])\n",
513 | " plt.title('SENSE')\n",
514 | "\n",
515 | " plt.subplot(133)\n",
516 | " plt.imshow(np.rot90(np.abs(image_cs[32:-32,32:-32]),-1),cmap='gray')\n",
517 | " plt.xticks([], [])\n",
518 | " plt.yticks([], [])\n",
519 | " plt.title('L1 - SENSE')\n",
520 | "\n"
521 | ],
522 | "metadata": {
523 | "id": "c4He8adsGb2v"
524 | },
525 | "execution_count": null,
526 | "outputs": []
527 | },
528 | {
529 | "cell_type": "markdown",
530 | "source": [
531 | "# Code to compare lamda values"
532 | ],
533 | "metadata": {
534 | "id": "e8zVi6UQNU1g"
535 | }
536 | },
537 | {
538 | "cell_type": "code",
539 | "source": [
540 | "# Grab the scan data\n",
541 | "kspace = kdata.copy()\n",
542 | "kspace /= np.max(np.abs(kspace))\n",
543 | "\n",
544 | "# Normalize the sensitivity maps (allows a step size of 1)\n",
545 | "maps = smaps.copy() / np.max(np.abs(smaps))\n",
546 | "\n",
547 | "kspace_us, mask = undersample_data(kspace, acc_y=1, kmax_y=1, kmax_x=1, random_undersampling_fraction=0.25)\n",
548 | "\n",
549 | "images = []\n",
550 | "error_k = []\n",
551 | "error_c = []\n",
552 | "for lp in np.linspace(-10,-5,20):\n",
553 | "\n",
554 | " l = 10**lp\n",
555 | "\n",
556 | " print(f'Working on {l}')\n",
557 | "\n",
558 | " # Actual reconstructions\n",
559 | " image_cs, error_kspace, error_constraint = iterative_sense(mask, kspace_us, maps, iterations=200, lamda=l, transform=edge_transform(), proximal=l1_prox)\n",
560 | "\n",
561 | " error_k.append(error_kspace[-1])\n",
562 | " error_c.append(error_constraint[-1])\n",
563 | " images.append(image_cs)\n",
564 | " \n",
565 | "\n",
566 | "plt.figure()\n",
567 | "plt.plot(error_k, error_c)\n",
568 | "plt.xlabel('Error Kspace')\n",
569 | "plt.ylabel('Error Constraint')\n",
570 | "plt.show()\n",
571 | "\n"
572 | ],
573 | "metadata": {
574 | "id": "bcF1VakfNYpG"
575 | },
576 | "execution_count": null,
577 | "outputs": []
578 | },
579 | {
580 | "cell_type": "code",
581 | "source": [
582 | "plt.figure(figsize=(20,10))\n",
583 | "plt.subplot(131)\n",
584 | "plt.imshow(np.rot90(np.abs(images[0][32:-32,32:-32]),-1),cmap='gray')\n",
585 | "plt.xticks([], [])\n",
586 | "plt.yticks([], [])\n",
587 | "plt.title('Low')\n",
588 | "\n",
589 | "plt.subplot(132)\n",
590 | "plt.imshow(np.rot90(np.abs(images[-4][32:-32,32:-32]),-1),cmap='gray')\n",
591 | "plt.xticks([], [])\n",
592 | "plt.yticks([], [])\n",
593 | "plt.title('Medium')\n",
594 | "\n",
595 | "plt.subplot(133)\n",
596 | "plt.imshow(np.rot90(np.abs(images[-1][32:-32,32:-32]),-1),cmap='gray')\n",
597 | "plt.xticks([], [])\n",
598 | "plt.yticks([], [])\n",
599 | "plt.title('High')\n"
600 | ],
601 | "metadata": {
602 | "id": "r8PJN44-i_Op"
603 | },
604 | "execution_count": null,
605 | "outputs": []
606 | },
607 | {
608 | "cell_type": "markdown",
609 | "source": [
610 | "# Code to compare transforms\n",
611 | "\n"
612 | ],
613 | "metadata": {
614 | "id": "ZkrmnKZ8nvI7"
615 | }
616 | },
617 | {
618 | "cell_type": "code",
619 | "source": [
620 | "# Grab the scan data\n",
621 | "kspace = kdata.copy()\n",
622 | "kspace /= np.max(np.abs(kspace))\n",
623 | "\n",
624 | "# Normalize the sensitivity maps (allows a step size of 1)\n",
625 | "maps = smaps.copy() / np.max(np.abs(smaps))\n",
626 | "\n",
627 | "# Subsample the data\n",
628 | "kspace_us, mask = undersample_data(kspace, acc_y=1, kmax_y=1, kmax_x=1, random_undersampling_fraction=0.25)\n",
629 | "\n",
630 | "images = []\n",
631 | "for transform in [None, edge_transform(), wavelet_transform()]:\n",
632 | " print(transform)\n",
633 | " # Actual reconstructions\n",
634 | " image_cs, error_kspace, error_constraint = iterative_sense(mask, kspace_us, maps, iterations=200, lamda=1e-6, transform=transform, proximal=l1_prox)\n",
635 | "\n",
636 | " images.append(image_cs)\n",
637 | "\n",
638 | "\n",
639 | "plt.figure(figsize=(20,10))\n",
640 | "plt.subplot(131)\n",
641 | "plt.imshow(np.rot90(np.abs(images[0][32:-32,32:-32]),-1),cmap='gray')\n",
642 | "plt.xticks([], [])\n",
643 | "plt.yticks([], [])\n",
644 | "plt.title('None')\n",
645 | "\n",
646 | "plt.subplot(132)\n",
647 | "plt.imshow(np.rot90(np.abs(images[1][32:-32,32:-32]),-1),cmap='gray')\n",
648 | "plt.xticks([], [])\n",
649 | "plt.yticks([], [])\n",
650 | "plt.title('Egde')\n",
651 | "\n",
652 | "plt.subplot(133)\n",
653 | "plt.imshow(np.rot90(np.abs(images[2][32:-32,32:-32]),-1),cmap='gray')\n",
654 | "plt.xticks([], [])\n",
655 | "plt.yticks([], [])\n",
656 | "plt.title('Wavelet')\n"
657 | ],
658 | "metadata": {
659 | "id": "6b_5ZnUTn4WK"
660 | },
661 | "execution_count": null,
662 | "outputs": []
663 | }
664 | ]
665 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Kevin M. Johnson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Matlab/bloch_nutation_solver.m:
--------------------------------------------------------------------------------
1 | function [time, Mout] = bloch_nutation_solver( event_list, M0, T1, T2, freq )
2 |
3 | % Inputs:
4 | % event_list -- Special structure with entries
5 | % .excite_flip flip angle of rotation
6 | % .excite_phase phase of excite degrees
7 | % .recovery_time time after excite to recover
8 | % .spoil (if 'true' this set the Mxy to zero at the recovery)
9 | % T1 -- Longitudinal relaxation times (s)
10 | % T2 -- Transverse relaxation times (s)
11 | % Freq Offset-- Off center frequency in Hz
12 | % M0 -- Initial state of magnetization (not equilibrium magnetization)
13 | % Outputs:
14 | % time -- Magnetization for each position in time
15 | % BOutput -- Magnetic field for each position in time (interpolated)
16 |
17 | % Initialize
18 | count = 1;
19 | time(1) = 0;
20 | Mout(1,:) = M0;
21 |
22 | M=M0;
23 |
24 | % Go through the event_list
25 | for pos = 1:numel(event_list)
26 |
27 | theta = event_list{pos}.excite_phase;
28 | alpha = event_list{pos}.excite_flip;
29 | T = event_list{pos}.recovery_time;
30 | spoil = event_list{pos}.spoil;
31 |
32 | % Excite
33 | Rz = [cosd(theta) sind(theta) 0;
34 | -sind(theta) cosd(theta) 0;
35 | 0 0 1];
36 | Rx = [1 0 0;
37 | 0 cosd(alpha) sind(alpha);
38 | 0 -sind(alpha) cosd(alpha)];
39 | M = inv(Rz)*Rx*Rz*M;
40 |
41 | % Relaxation (Transverse)
42 | if spoil
43 | Mxy = 0;
44 | else
45 | Mxy = M(1) + 1i*M(2);
46 | Mxy = Mxy*exp( 2i*pi*freq*T)*exp(-T/T2);
47 | end
48 |
49 | % Relaxation (Longitudinal)
50 | Mz = M(3);
51 | Mz = 1 + (Mz - 1)*exp(-T/T1);
52 |
53 | % Put back into [Mx; My; Mz] vector
54 | M = [real(Mxy); imag(Mxy); Mz];
55 |
56 | % Store for output
57 | count = count+1;
58 | time(count) = time(count-1)+T;
59 | Mout(count,:) = M;
60 | end
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/Matlab/bloch_solver.m:
--------------------------------------------------------------------------------
1 | function [MOutput, BOutput] = bloch_solver( B, time, freq, T1, T2, M0)
2 | % This is simple Rk4 solution to the Bloch Equations.
3 | %
4 | % Inputs:
5 | % B -- Magentic Field (N x 3) (T)
6 | % time -- Time of each point in waveforms (s)
7 | % T1 -- Longitudinal relaxation times (s)
8 | % T2 -- Transverse relaxation times (s)
9 | % Freq Offset-- Off center frequency in Hz
10 | % M0 -- Initial state of magnetization (not equilibrium magnetization)
11 | % Outputs:
12 | % MOutput -- Magnetization for each position in time
13 | % BOutput -- Magnetic field for each position in time (interpolated)
14 |
15 | % Assume protons
16 | GAM = 42.58e6*2*pi;
17 |
18 | % Storage for solution
19 | MxOutput = [];
20 | MyOutput = [];
21 | MzOutput = [];
22 |
23 | % Convert frequency to rads/s
24 | act_freq = 2*pi*freq;
25 |
26 | %Convert to B1
27 | Bx = B(:,1);
28 | By = B(:,2);
29 | Bz = B(:,3) + act_freq/GAM; %a frequency offset is a Bz offset
30 |
31 | %Create a spline for interpolation
32 | Bs = spline(time,GAM*[Bx(:)'; By(:)'; Bz(:)']);
33 |
34 | %Initialize
35 | Mag = M0;
36 |
37 | count = 1;
38 |
39 | %Runge-Kutta PDE Solution
40 | tvals = linspace(min(time),max(time),length(time));
41 | dt = tvals(2) - tvals(1);
42 | for t1 = tvals
43 |
44 | m1 = Mag;
45 | b = ppval(Bs,t1);
46 | k1 = [ -1/T2 b(3) -b(2);
47 | -b(3) -1/T2 b(1);
48 | b(2) -b(1) -1/T1]*m1 + [0; 0; 1/T1];
49 |
50 | t2 = t1 + dt/2;
51 | b = ppval(Bs,t2);
52 | m2 = Mag + k1*dt/2;
53 | k2 = [ -1/T2 b(3) -b(2);
54 | -b(3) -1/T2 b(1);
55 | b(2) -b(1) -1/T1]*m2 + [0; 0; 1/T1];
56 |
57 | t3 = t1 + dt/2;
58 | m3 = Mag + k2*dt/2;
59 | k3 = [ -1/T2 b(3) -b(2);
60 | -b(3) -1/T2 b(1);
61 | b(2) -b(1) -1/T1]*m3 + [0; 0; 1/T1];
62 |
63 |
64 | t4 = t1 + dt;
65 | b = ppval(Bs,t2);
66 | m4 = Mag + k3*dt;
67 | k4 = [ -1/T2 b(3) -b(2);
68 | -b(3) -1/T2 b(1);
69 | b(2) -b(1) -1/T1]*m4 + [0; 0; 1/T1];
70 |
71 | Mag = Mag + dt/6*(k1 + 2*k2 + 2*k3 + k4);
72 |
73 | % Save output of magnetization
74 | MOutput(:,count)= Mag;
75 |
76 | % Save Output of magnetic field
77 | BOutput(:,count)= b;
78 |
79 | count = count+1;
80 | end
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Matlab/bloch_solver_examples.m:
--------------------------------------------------------------------------------
1 | % Example : T1 recovery from 0 magnetization
2 | M0 = [0; 0; 0];
3 | time = 0:1e-3:2;
4 | B = zeros(length(time),3);
5 | freq = 0;
6 | T1 = 1; %s
7 | T2 = 1; %s
8 | [Mout,Bout] = bloch_solver( B, time, freq,T1, T2, M0);
9 | figure
10 | plot(time,Mout)
11 | ylabel('Magnetizaton [a.u.]');
12 | xlabel('Time [s]');
13 | legend('Mx','My','Mz')
14 |
15 | % Example : T2 decay
16 | M0 = [1; 0; 0];
17 | time = 0:1e-4:2;
18 | B = zeros(length(time),3);
19 | freq = 100;
20 | T1 = 1; %s
21 | T2 = 0.1; %s
22 | [Mout,Bout] = bloch_solver( B, time, freq,T1, T2, M0);
23 | figure
24 | plot(time,Mout)
25 | ylabel('Magnetizaton [a.u.]');
26 | xlabel('Time [s]');
27 | legend('Mx','My','Mz')
28 |
29 |
30 | % Example : RF Excite starting from thermodynamic equilibrium
31 | M0 = [0; 0; 1];
32 | time = 0:1e-5:0.1;
33 | B = zeros(length(time),3);
34 | T = 8e-3; %8ms pulse
35 | flip = 30; %30 degree flip
36 | B( (time>=1e-3) & (time<=1e-3+T),1)= (30/360)/T/42.58e6;
37 | freq = 0;
38 | T1 = 1; %s
39 | T2 = 0.1; %s
40 | [Mout,Bout] = bloch_solver( B, time, freq,T1, T2, M0);
41 | figure
42 | plot(time,Mout)
43 | ylabel('Magnetizaton [a.u.]');
44 | xlabel('Time [s]');
45 | legend('Mx','My','Mz')
--------------------------------------------------------------------------------
/Matlab/rf_simulator_example.m:
--------------------------------------------------------------------------------
1 | clear
2 | clc
3 | close all
4 |
5 | %Settable parameters
6 | TBW = 8; % time bandwidth product (unitless)
7 | T = 2e-3; %total time of pulse (s)
8 | thickness = 0.1; % selection thickness (m)
9 | gamma = (42.58e6); % gamma (has to be this since bloch sim doesn't take input)
10 | flip = 90; % flip angle (degrees)
11 |
12 | % 8us spacing is reasonable
13 | dT = 8e-6;
14 |
15 | % Intial state of magnetization
16 | M0 = [0; 0; 1];
17 |
18 | % Time
19 | time = 0:dT:T;
20 |
21 | % Sinc pulse with 2 sidelobes
22 | B1 = sinc(linspace(-TBW/2,TBW/2,numel(time)))';
23 |
24 | %Get the BW
25 | BW = TBW/(max(time));
26 |
27 | % Plot frequency content of B1 pulse
28 | N = numel(B1);
29 | SamplingBW = 1/(dT);
30 | f = linspace(-SamplingBW/2,SamplingBW/2,10000);
31 | figure
32 | dft_of_b1 = abs(fftshift(fft(B1,10000)));
33 | ideal_b1 = double(abs(f) < BW/2);
34 | dft_of_b1 = dft_of_b1*(ideal_b1(:)'*dft_of_b1(:))/(dft_of_b1(:)'*dft_of_b1(:)); % just to scale nicely
35 | plot(f,dft_of_b1);
36 | hold on
37 | plot(f,ideal_b1)
38 | xlim([-BW BW]);
39 | ylim([0 1.2]);
40 | xlabel('Frequency [Hz]')
41 | ylabel('Value [a.u.]');
42 | title(['Fourier Tranform of B1 Pulse (TBW=',num2str(TBW),',BW=',num2str(BW),')'])
43 |
44 | % Constant gradient
45 | Gamp = BW / ( gamma*thickness);
46 | G = Gamp*ones(numel(time),1);
47 |
48 | % Add rephasing gradient and som zero values at the end
49 | G = [G; -0.5*G; 0.0*G];
50 | B1= [B1; 0*B1; 0*B1];
51 | time = dT*(0:numel(G)-1)';
52 |
53 | % Calculate over +/- 10cm
54 | zloc = linspace(-thickness,thickness,101);
55 |
56 | % Offset frequency
57 | freq = 0;
58 | T1 = 100000;
59 | T2 = 100000;
60 |
61 | % set flip to 90
62 | B1 = B1*(flip/180*pi)/(2*pi*42.58e6*sum(real(B1)*dT));
63 |
64 | figure
65 | subplot(121)
66 | plot(time,B1);
67 | xlabel('Time [s]')
68 | ylabel('B_1 [T]')
69 | xlim([min(time) max(time)])
70 | subplot(122)
71 | plot(time,G);
72 | xlabel('Time [s]')
73 | ylabel('G [T/m]')
74 | xlim([min(time) max(time)])
75 |
76 | for z_index = 1:numel(zloc)
77 | % Setup magnatic field
78 | B(:,1) = real(B1);
79 | B(:,2) = imag(B1);
80 | B(:,3) = G.*zloc(z_index);
81 |
82 | % Run simulator
83 | [Mout,Bout] = bloch_solver( B, time(:),freq(:),T1, T2, M0);
84 | % Mx is [1 (freq) x 101 (z) x 251 (time)]
85 | Mx(z_index,:) = Mout(1,:);
86 | My(z_index,:) = Mout(2,:);
87 | Mz(z_index,:) = Mout(3,:);
88 | end
89 |
90 | figure
91 | plot(zloc,squeeze(abs(Mx(:,end)+1i*My(:,end))))
92 | xlabel('Z Location [m]');
93 | ylabel('M_x_y [1/M_0]');
94 |
95 |
96 | figure
97 | title('M_x_y During RF');
98 | imagesc(time,zloc,squeeze(abs(Mx(:,:)+1i*My(:,:))))
99 | ylabel('Z Location [m]');
100 | xlabel('Time [s]');
101 | title('M_x_y')
102 | colorbar
103 |
104 | figure
105 | imagesc(time,zloc,squeeze(angle(Mx(:,:)+1i*My(:,:))))
106 | ylabel('Z Location [m]');
107 | xlabel('Time [s]');
108 | title('\angle{M_x_y} During RF');
109 | colorbar
110 |
111 | figure
112 | title('M_z During RF');
113 | imagesc(time,zloc,squeeze(Mz(:,:)))
114 | ylabel('Z Location [m]');
115 | xlabel('Time [s]');
116 | title('M_z')
117 | colorbar
118 |
--------------------------------------------------------------------------------
/NoteBooks/BasicWeightedImages.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "provenance": [],
7 | "authorship_tag": "ABX9TyOwcCuaLBaVTtUR/CKtQVtp",
8 | "include_colab_link": true
9 | },
10 | "kernelspec": {
11 | "name": "python3",
12 | "display_name": "Python 3"
13 | },
14 | "language_info": {
15 | "name": "python"
16 | }
17 | },
18 | "cells": [
19 | {
20 | "cell_type": "markdown",
21 | "metadata": {
22 | "id": "view-in-github",
23 | "colab_type": "text"
24 | },
25 | "source": [
26 | "
"
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "source": [
32 | "# Basic MRI Contrast Images\n",
33 | "\n",
34 | "This notebook will simulate image for Gradient Echo and Spin Echo sequences. It uses a digital phantom of the brain:\n",
35 | "\n",
36 | "* http://www.bic.mni.mcgill.ca/brainweb/\n",
37 | "\n",
38 | "* C.A. Cocosco, V. Kollokian, R.K.-S. Kwan, A.C. Evans : \"BrainWeb: Online Interface to a 3D MRI Simulated Brain Database\"\n",
39 | "NeuroImage, vol.5, no.4, part 2/4, S425, 1997 -- Proceedings of 3-rd International Conference on Functional Mapping of the Human Brain, Copenhagen, May 1997.\n",
40 | "\n",
41 | "* R.K.-S. Kwan, A.C. Evans, G.B. Pike : \"MRI simulation-based evaluation of image-processing and classification methods\"\n",
42 | "IEEE Transactions on Medical Imaging. 18(11):1085-97, Nov 1999.\n",
43 | "R.K.-S. Kwan, A.C. Evans, G.B. Pike : \"An Extensible MRI Simulator for Post-Processing Evaluation\"\n",
44 | "\n",
45 | "* Visualization in Biomedical Computing (VBC'96). Lecture Notes in Computer Science, vol. 1131. Springer-Verlag, 1996. 135-140.\n",
46 | "\n",
47 | "* D.L. Collins, A.P. Zijdenbos, V. Kollokian, J.G. Sled, N.J. Kabani, C.J. Holmes, A.C. Evans : \"Design and Construction of a Realistic Digital Brain Phantom\"\n",
48 | "IEEE Transactions on Medical Imaging, vol.17, No.3, p.463--468, June 1998.\n",
49 | "\n",
50 | "## Limitations\n",
51 | "\n",
52 | "* The $T1$ and $T2$ values are assumed to be the same for each tissue type.\n",
53 | "* The $T2'$ (and subsequently $T2^*$) are calculated from a simluation based on the segmentation of the anatomy and assumed magnetic suceptibility. It therfore, does not do a great job in replicating $T2^*$ seen in-vio.\n"
54 | ],
55 | "metadata": {
56 | "id": "UHmNu4Fznltu"
57 | }
58 | },
59 | {
60 | "cell_type": "code",
61 | "execution_count": null,
62 | "metadata": {
63 | "id": "1rufIdmlc2ro"
64 | },
65 | "outputs": [],
66 | "source": [
67 | "# For interactive plotting\n",
68 | "from ipywidgets import interact, interactive, FloatSlider, IntSlider, ToggleButton\n",
69 | "from IPython.display import clear_output, display, HTML\n",
70 | "\n",
71 | "# General utilities\n",
72 | "import numpy as np\n",
73 | "import glob\n",
74 | "import h5py\n",
75 | "\n",
76 | "# for plotting modified style for better visualization\n",
77 | "import matplotlib.pyplot as plt\n",
78 | "import matplotlib as mpl\n",
79 | "mpl.rcParams['lines.linewidth'] = 4\n",
80 | "mpl.rcParams['axes.titlesize'] = 16\n",
81 | "mpl.rcParams['axes.labelsize'] = 14\n",
82 | "mpl.rcParams['xtick.labelsize'] = 12\n",
83 | "mpl.rcParams['ytick.labelsize'] = 12\n",
84 | "mpl.rcParams['legend.fontsize'] = 12\n",
85 | "\n",
86 | "\n",
87 | "# For interactive plotting\n",
88 | "from ipywidgets import interact, interactive, FloatSlider, IntSlider, ToggleButton\n",
89 | "from IPython.display import clear_output, display, HTML\n",
90 | "\n",
91 | "# for plotting modified style for better visualization\n",
92 | "import matplotlib.pyplot as plt\n",
93 | "import matplotlib as mpl\n",
94 | "mpl.rcParams['lines.linewidth'] = 4\n",
95 | "mpl.rcParams['axes.titlesize'] = 16\n",
96 | "mpl.rcParams['axes.labelsize'] = 14\n",
97 | "mpl.rcParams['xtick.labelsize'] = 12\n",
98 | "mpl.rcParams['ytick.labelsize'] = 12\n",
99 | "mpl.rcParams['legend.fontsize'] = 12\n"
100 | ]
101 | },
102 | {
103 | "cell_type": "markdown",
104 | "source": [
105 | "# Download the Digitial Phantom \n",
106 | "This will download a tar containing multiple HDF5 files. Each file represents a tissue with one additional file to provide T2' values."
107 | ],
108 | "metadata": {
109 | "id": "0UCZ_bhHpeD0"
110 | }
111 | },
112 | {
113 | "cell_type": "code",
114 | "source": [
115 | "# Download a tar with tissues\n",
116 | "!wget -O brain_sim.tar https://www.dropbox.com/scl/fi/uuf51rdqcb3ml44xt3uz5/brain_sim.tar?rlkey=t7plwqqr5i3qba66wbbw0i1z3&dl=0\n",
117 | "!tar xvf brain_sim.tar"
118 | ],
119 | "metadata": {
120 | "id": "DOwI8xCKmNUl"
121 | },
122 | "execution_count": null,
123 | "outputs": []
124 | },
125 | {
126 | "cell_type": "markdown",
127 | "source": [
128 | "# Define a Tissue Class\n",
129 | "Tissues are represented by a structure which contains its name, a volume, and relevant parameters. The volume defines a fuzzy segmentation of the tissue. It also defined loading and saving functions from the HDF5 file."
130 | ],
131 | "metadata": {
132 | "id": "vbXy_zkrp_p8"
133 | }
134 | },
135 | {
136 | "cell_type": "code",
137 | "source": [
138 | "class Tissue:\n",
139 | " '''\n",
140 | " Tissue class to store MRI data and parameters\n",
141 | " '''\n",
142 | "\n",
143 | " def __init__(self, volume):\n",
144 | " self.volume = volume\n",
145 | " self.T1 = -1\n",
146 | " self.T2 = -1\n",
147 | " self.T2star = -1\n",
148 | " self.density = -1\n",
149 | " self.susceptibility = -1\n",
150 | " self.name = 'None'\n",
151 | " self.freq = 0.0\n",
152 | "\n",
153 | " def __getitem__(self, idx):\n",
154 | " return ((self.volume[idx] + 128) / 255)\n",
155 | "\n",
156 | " def __setitem__(self, idx, value):\n",
157 | " self.volume[idx] = value\n",
158 | "\n",
159 | " def export(self, filename):\n",
160 | "\n",
161 | " with h5py.File(filename, 'w') as f:\n",
162 | " f.create_dataset('volume', data=self.volume, compression='gzip')\n",
163 | " f.attrs['T1'] = self.T1\n",
164 | " f.attrs['T2'] = self.T2\n",
165 | " f.attrs['T2star'] = self.T2star\n",
166 | " f.attrs['density'] = self.density\n",
167 | " f.attrs['susceptibility'] = self.susceptibility\n",
168 | " f.attrs['name'] = self.name\n",
169 | " f.attrs['freq'] = self.freq\n",
170 | "\n",
171 | " def load(self, filename):\n",
172 | "\n",
173 | " with h5py.File(filename, 'r') as f:\n",
174 | " self.volume = np.array(f['volume']).astype(np.float32)\n",
175 | " self.T1 = f.attrs['T1']\n",
176 | " self.T2 = f.attrs['T2']\n",
177 | " self.T2star = f.attrs['T2star']\n",
178 | " self.density = f.attrs['density']\n",
179 | " self.susceptibility = f.attrs['susceptibility']\n",
180 | " self.name = f.attrs['name']\n",
181 | " self.freq = f.attrs['freq']\n",
182 | "\n"
183 | ],
184 | "metadata": {
185 | "id": "PTLZmFgtghrp"
186 | },
187 | "execution_count": null,
188 | "outputs": []
189 | },
190 | {
191 | "cell_type": "markdown",
192 | "source": [
193 | "# Load Tissues files and the gradient files"
194 | ],
195 | "metadata": {
196 | "id": "VLZYATbhqfwE"
197 | }
198 | },
199 | {
200 | "cell_type": "code",
201 | "source": [
202 | "files = glob.glob('subject54*.h5')\n",
203 | "tissues = []\n",
204 | "for file in files:\n",
205 | " tissue = Tissue(None)\n",
206 | " tissue.load(file)\n",
207 | " tissues.append(tissue)\n",
208 | "\n",
209 | "\n",
210 | "with h5py.File('gradient.h5','r') as hf:\n",
211 | " grad = np.array(hf['gradient']).astype(np.float32)\n",
212 | "grad /= np.max(grad)"
213 | ],
214 | "metadata": {
215 | "id": "sUkbc_W8jM7Q"
216 | },
217 | "execution_count": null,
218 | "outputs": []
219 | },
220 | {
221 | "cell_type": "markdown",
222 | "source": [
223 | "# Define the simulation and plotting functions\n",
224 | "This will define a function to calculate the signal from the volume and imaging parameters. The simulation is based on analytical solutions to be fast simulating:\n",
225 | "\n",
226 | "* **Spoiled Gradient Echo** : This is based on a simple steady state solution for $M_z$ with $M_{xy}$ cacluated from $M_z$ and the gradient of the simulated field.\n",
227 | "\n",
228 | "* **Spin echo** : This is a for a $90^\\circ$ - $180^\\circ$ sequence. The flip angle thus cannot be set by the user and will automatically be set to $90^\\circ$.\n"
229 | ],
230 | "metadata": {
231 | "id": "XgtOmi7iqwsL"
232 | }
233 | },
234 | {
235 | "cell_type": "code",
236 | "source": [
237 | "def calc_signal( TE, TR, B0, freq, alpha, M0, T1, T2, T2star, spin_echo, grad_slice):\n",
238 | " # T1, T2, T2star in ms\n",
239 | " # B0 in Hz\n",
240 | " # alpha in degrees\n",
241 | " # M0 in arbitrary units\n",
242 | " # TE, TR in ms\n",
243 | "\n",
244 | " # convert B0 to rad/s\n",
245 | " B0 = B0 * 2 * np.pi\n",
246 | "\n",
247 | " # convert alpha to rad\n",
248 | " alpha = alpha * np.pi / 180\n",
249 | "\n",
250 | " # calculate the longitudinal magnetization based on flip angle\n",
251 | " #Mxy = Mz * np.sin(alpha) * np.exp(-TE / T2star)\n",
252 | " if spin_echo:\n",
253 | " Mz180 = -M0*(1 - np.exp(-0.5*TE/T1))\n",
254 | " Mz = M0 + (Mz180 - M0) * np.exp(-(TR-0.5*TE) / T1)\n",
255 | " Mxy = Mz * np.exp(-TE / T2)\n",
256 | " else:\n",
257 | " Mz = M0 * (1 - np.exp(-TR / T1)) / (1 - np.cos(alpha) * np.exp(-TR / T1))\n",
258 | " Mxy = Mz * np.sin(alpha) * np.exp(-TE / T2star) * np.exp(-20*grad_slice*TE) * np.exp(-1j*2*np.pi*freq*TE/1000)\n",
259 | "\n",
260 | " return Mxy\n",
261 | "\n",
262 | "\n",
263 | "def plot_image(TR, TE, flip, slice, spin_echo, noise_level):\n",
264 | "\n",
265 | "\n",
266 | " #print(f'{TR}, {TE}, {flip}')\n",
267 | "\n",
268 | " signal = np.zeros((434, 362), dtype=np.complex128)\n",
269 | " for tissue in tissues:\n",
270 | " signal += calc_signal(TE, TR, 0, tissue.freq, flip, tissue[slice]*tissue.density, tissue.T1, tissue.T2, tissue.T2star, spin_echo, grad[slice])\n",
271 | "\n",
272 | " signal = np.abs( signal + np.random.normal(0, noise_level, signal.shape))\n",
273 | "\n",
274 | " plt.figure()\n",
275 | " plt.imshow(np.flip(signal), cmap='gray')\n",
276 | "\n",
277 | " #plt.imshow(np.flip(phs_tissue_simulated[slice]), cmap='gray')\n",
278 | "\n",
279 | " plt.xticks([])\n",
280 | " plt.yticks([])\n",
281 | "\n",
282 | " if spin_echo:\n",
283 | " plt.title(f' SE:flip={int(flip)},TR={int(TR)},TE={int(TE)} ')\n",
284 | " else:\n",
285 | " plt.title(f'GRE:flip={int(flip)},TR={int(TR)},TE={int(TE)} ')\n",
286 | "\n",
287 | " plt.colorbar()\n",
288 | " plt.show()\n",
289 | "\n",
290 | "\n",
291 | "\n",
292 | "TRslider = FloatSlider(min=2, max=5000, step=1, value=50, description='TR [ms]',continuous_update=True)\n",
293 | "TEslider = FloatSlider(min=1, max=50, step=1, value=3, description='TE [ms]',continuous_update=True)\n",
294 | "flip_slider = FloatSlider(min=1, max=90, step=1, value=10, description='Flip [deg.]',continuous_update=True)\n",
295 | "spin_echo_toggle = ToggleButton(value=False, description='Toggle Spin echo', continuous_update=True)\n",
296 | "\n",
297 | "def update_max_TE(change):\n",
298 | " TEslider.max = min(change['new']-1,100.0)\n",
299 | "\n",
300 | "def update_flip_min(change):\n",
301 | "\n",
302 | " if change['new']:\n",
303 | " flip_slider.max = 90\n",
304 | " flip_slider.min = 90\n",
305 | " else:\n",
306 | " flip_slider.max = 90\n",
307 | " flip_slider.min = 1\n",
308 | "\n",
309 | "\n",
310 | "TRslider.observe(update_max_TE, names='value')\n",
311 | "spin_echo_toggle.observe(update_flip_min, names='value')"
312 | ],
313 | "metadata": {
314 | "id": "IdvlxlvigyGP"
315 | },
316 | "execution_count": null,
317 | "outputs": []
318 | },
319 | {
320 | "cell_type": "markdown",
321 | "source": [
322 | "# Main simulation\n",
323 | "Things to try:\n",
324 | "\n",
325 | "1. Set the flip angle to 90 and the TE to 1. Sweep the TR. Does the contrast behave as expected? What type of weighting might this be?\n",
326 | "\n",
327 | "1. Set the flip angle to 90 and TR to 5000. Sweep the TE. Does the contrast behave as expected? What type of weighting might this be?\n",
328 | "\n",
329 | "1. Try 2. Again but swap between spin and gradient echo images. Look at multiple slices to see differences.\n",
330 | "\n",
331 | "1. For gradient echo, Set the TR to 10 and TE to 1. Sweep the flip angle. Does the contrast behave as expected? What type of weighting might this be?\n",
332 | "\n",
333 | "1. Mix the changes in TE, TR, and flip angle.\n",
334 | "\n",
335 | "\n"
336 | ],
337 | "metadata": {
338 | "id": "NiAuEhx1sInp"
339 | }
340 | },
341 | {
342 | "cell_type": "code",
343 | "source": [
344 | "w = interactive(plot_image,\n",
345 | " TR=TRslider,\n",
346 | " TE=TEslider,\n",
347 | " flip=flip_slider,\n",
348 | " slice=IntSlider(min=0, max=362, step=1, value=130, description='Slice',continuous_update=True),\n",
349 | " spin_echo=spin_echo_toggle,\n",
350 | " noise_level=FloatSlider(min=0, max=0.1, step=0.001, value=0.01, description='Noise level',continuous_update=True)\n",
351 | " )\n",
352 | "display(w)"
353 | ],
354 | "metadata": {
355 | "id": "ZC6-RgeWkZrD"
356 | },
357 | "execution_count": null,
358 | "outputs": []
359 | }
360 | ]
361 | }
--------------------------------------------------------------------------------
/NoteBooks/Intro_to_Jupyter.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "name": "Intro_to_Jupyter.ipynb",
7 | "version": "0.3.2",
8 | "provenance": [],
9 | "collapsed_sections": [],
10 | "include_colab_link": true
11 | },
12 | "kernelspec": {
13 | "name": "python3",
14 | "display_name": "Python 3"
15 | }
16 | },
17 | "cells": [
18 | {
19 | "cell_type": "markdown",
20 | "metadata": {
21 | "id": "view-in-github",
22 | "colab_type": "text"
23 | },
24 | "source": [
25 | "
"
26 | ]
27 | },
28 | {
29 | "metadata": {
30 | "id": "rByi0eBvpEGT",
31 | "colab_type": "code",
32 | "colab": {}
33 | },
34 | "cell_type": "code",
35 | "source": [
36 | "# In Python you import libraries to use\n",
37 | "\n",
38 | "# Numpy is an array library \n",
39 | "import numpy as np\n",
40 | "\n",
41 | "# Matplotlib is a plotting library\n",
42 | "import matplotlib.pyplot as plt\n",
43 | "\n",
44 | "# These are core pyhton libaries\n",
45 | "import math"
46 | ],
47 | "execution_count": 0,
48 | "outputs": []
49 | },
50 | {
51 | "metadata": {
52 | "id": "ZZK5uiwJpsLA",
53 | "colab_type": "code",
54 | "colab": {}
55 | },
56 | "cell_type": "code",
57 | "source": [
58 | "# Make a simple array \n",
59 | "t = np.linspace(start=0, stop=2*math.pi, num=100, dtype=np.float32)\n",
60 | "y = np.sin(t)"
61 | ],
62 | "execution_count": 0,
63 | "outputs": []
64 | },
65 | {
66 | "metadata": {
67 | "id": "4gpoppbZqwJe",
68 | "colab_type": "code",
69 | "colab": {
70 | "base_uri": "https://localhost:8080/",
71 | "height": 361
72 | },
73 | "outputId": "9deb6c3e-7a9f-456b-e6ab-a33750d594ad"
74 | },
75 | "cell_type": "code",
76 | "source": [
77 | "# Simple plot\n",
78 | "plt.figure()\n",
79 | "plt.plot(t,y)\n",
80 | "plt.xlabel('Time')\n",
81 | "plt.ylabel('Signal')\n",
82 | "plt.show()"
83 | ],
84 | "execution_count": 7,
85 | "outputs": [
86 | {
87 | "output_type": "display_data",
88 | "data": {
89 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAfsAAAFYCAYAAABUA1WSAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4yLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvNQv5yAAAIABJREFUeJzs3XlcVOe9P/DPmRmGbdiZAWQXFRRF\nxQ3BLUaj4hKzEDExSXvT3KZNmrQ/TbWm95p7m5ikr9gtN+1tvEmztYklcd+jwQVFcUEUXFBkR2AG\nkH2ZYc7vDxMaoyLqMM8sn/fr1Vc9M3NmPufJ6Hee85zzPJIsyzKIiIjIYSlEByAiIqL+xWJPRETk\n4FjsiYiIHByLPRERkYNjsSciInJwLPZEREQOTiU6QH/R65st+n5+fh5oaGiz6HvaG2dvA2c/foBt\n4OzHD7ANANttA63W65bPsWffRyqVUnQE4Zy9DZz9+AG2gbMfP8A2AOyzDVjsiYiIHByLPRERkYNj\nsSciInJwLPZEREQOjsWeiIjIwbHYExEROTgWeyIiIgfHYk9EROTghBT7wsJCzJgxA59++ukNzx0+\nfBiPPvooFi1ahHfffbfn8dWrV2PRokVIT0/H6dOnrRmXiIjIrll9uty2tjb85je/wcSJE2/6/Guv\nvYb3338fQUFBWLJkCWbNmoX6+nqUlpZi3bp1KCoqwsqVK7Fu3TorJyciIrJPVi/2arUaa9euxdq1\na294rry8HD4+PggJCQEATJ06FdnZ2aivr8eMGTMAADExMWhsbERLSws0Go1Vs5M4ncZuXKlrhf5q\nB7qM3TCazOgymWE2y/DycIGvxhW+GjX8vFzh4eYiOi4RkU2xerFXqVRQqW7+sXq9Hv7+/j3b/v7+\nKC8vR0NDA+Lj4697XK/X91rs/fw8LD5/cW+LDDgLa7SBLMuoqG3BsbPVyL9ch/KaZtTUt0GW+7Z/\ncIAHhkUHYFi0P4ZFByBMp4EkSRbJxu8A28DZjx9gGwD21wZ2ueqd3Id/9S29IpFW62XxlfTsTX+3\nQfGVJhw9W4NTlwyobWjvedzLwwWx4b4YEOiJIH8PuLko4eKigItSCYUCaG4z4mpLJ662dMHQ2I7i\nqiZ8fbwcXx8vBwAE+blj4vBgTIwPhtbX/a7z8TvANnD24wfYBoDttkFvP0BsqtjrdDoYDIae7Zqa\nGuh0Ori4uFz3eG1tLbRarYiIZGGmbjOOX6jF3uMVKKpqAgC4qpUYE6vFyJhADB/oD1+N6x29p1mW\nccXQiosVjThX2oC8SwZsPFiMjQeLMSTMB/clhmFcnA4KhWV6+0REts6min1YWBhaWlpQUVGB4OBg\nZGZm4u2330ZDQwPeeecdpKeno6CgADqdjuP1ds5o6sbeE5XYfawMV1u6IAEYGROA+xJDMTTSHy6q\nu79RRCFJCNVqEKrVYNroULR3mnDigh6H86/gQtlVFFY0YvOhYsxPjsL4oUEs+kTk8Kxe7PPz8/HW\nW2+hsrISKpUKu3btwvTp0xEWFoaZM2fi1VdfxdKlSwEAqampiI6ORnR0NOLj45Geng5JkrBq1Spr\nxyYLkWUZx87XIiOzCHVNHXB3VWLm2HBMHxOKID+PfvlMd1cVJiWEYFJCCGob2rA1uxTZ+dV4b8tZ\nbD5UgoenDMSYWK3FxvWJiGyNJPdlANwOWXo8xVbHaKzpXtugqKoRn++5iKKqJigVEmaMDcO85Ch4\nCrh6Xn+1HduyS3DoTDW6zTISYgKwZOYQBPYyps/vANvA2Y8fYBsAttsGdjNmT47JaDJjY9Zl7DxS\nBhnA2FgtHp0WA10/9eT7Quvrjh/MGYo5EyLx8a4LOF1Uh1+XHcWDk6Ixc2w4VEpOLklEjoPFnvpV\neW0L1m45iwp9C7S+bvi31KGIjfATHatHkL8HlqWPwpGCGny29yIyMouQc64WP3kwXuiPESIiS2Kx\np34hyzJ25pRh/f7L6DbLmDZqAB6bPghuatv7ykmShInDgzEiJgCf772Iw/nV+K8Pj+GHc4ZibJxO\ndDwiontme//ykt3r6DLhg23ncPyCHj4aNX44Jw4JMYGiY92Wxt0FP5o3DEMj/fDJ7gv488Z83J8Y\nhsemD7qnuwOIiERjsSeL0l9txztfnkGFvgVDwn3x04XD4e2pFh3rjqSMCEF0iDf+sjEfe09W4PKV\nRrz4SILdzZhFRPQtdlfIYs6VNuA3Hx1Hhb4F940OxbL0UXZX6L81INATv356LJKHB6P4SjNe/+QE\nymts7+pbIqK+YLEnizh6tga/W3cK7Z0mPDUrFk/OirX7K9pdXZR4Zu5QLJwcDUNjB15+5yAulDWI\njkVEdMfs+19jsgn7civx3uYCqF0UWJY+CtNGh4qOZDGSJGFBSjSemTsUHZ0mrFl3CkfP1oiORUR0\nR1js6Z5syy7Bx7suQOPhgl8uTrSp2+osKWVECP7r2YlwUSnw3uYCHMyrEh2JiKjPWOzprsiyjIx9\nl/Dl/svw93bFiicSERns2BewjRyixfLHE+Hp7oK/7TiPAyz4RGQnWOzpjsmyjC/2F2HHkTIE+Xvg\nV0+MQUiAp+hYVhER5IWXF4+Gxt0FH+44j32nKkVHIiK6LRZ7umNbs0t7Cv2Kx0cjwMdNdCSrCtdp\n8MtvCv7HOy9gXy4LPhHZNhZ7uiNfHSvHhgOXEeDthpfTR8HnDteadxRhOg1++fhoeHm44ONdF5Bd\nUC06EhHRLbHYU599dbQUn+29CB9PNZYtHgV/b+fq0X9fmFaDl9NHw91VhQ+2nUN+cZ3oSEREN8Vi\nT31yslCPdzJOQePugmXpo/pt7Xl7E6bT4MVHRkCSJLy7Ph/FV5pERyIiugGLPd1W8ZUmvLe5AK4u\nSvzisZEI1WpER7IpsRF++PGCeHSZuvGHjDzU1LeJjkREdB0We+qVobEdf/riNIzdZry8ZCyiQ7xF\nR7JJY2K1ePKBWDS3GbFm3Sk0tXaJjkRE1IPFnm6prcOEP35xGo2tXUi/fzDGxweLjmTTpo0OxYKU\nKBgaO/DuhjMwdZtFRyIiAsBiT7dg6jbjLxvPoFLfivvHhGHm2HDRkezCg5OiMTZOh4sVjfh0dyFk\nWRYdiYiIxZ5u7p+Zl1BQ0oCEmAAsvn+w6Dh2Q5IkPJM6FBFBGhzIq8LXJ3kPPhGJx2JPNzhythp7\njlcgJMADP14QD4VCEh3JrriqlfjZwwnw9nDBZ3su4lxJvehIROTkWOzpOhW1Lfhwx3m4qZV44eER\ncHdViY5klwJ83PD8wyMgScCfN+ZDf7VddCQicmIs9tSjrcOI/1l/Bl1GM56ZO8xp5rvvL4PDfPHk\nrFi0dpjwl435MJp4wR4RicFiTwAAsyxj7ZazqL3ajtSkSIyJ1YqO5BCmjByAlBHBKKluxj8zL4mO\nQ0ROisWeAAA7jpQir6gOw6L88PCUgaLjOJQlD8QiNNATe09U4Pj5WtFxiMgJCRmQXb16NfLy8iBJ\nElauXImEhAQAQE1NDZYtW9bzuvLycixduhRGoxF//OMfERERAQBITk7GT37yExHRHVJRZSM2HCiG\nn5crL8jrB64uSvxk4XD85qPj+NuOc4gI0kDH6YaJyIqsXuxzcnJQWlqKdevWoaioCCtXrsS6desA\nAEFBQfjkk08AACaTCU8++SSmT5+OXbt2ITU1FcuXL7d2XIfX1mHCXzcXQJZlPDtvGLw81KIjOaQB\ngZ54alYs1m49iz9vzMcrT46Bi0opOhYROQmrn8bPzs7GjBkzAAAxMTFobGxES0vLDa/bsGEDZs2a\nBU9PXiTWX2RZxse7zsPQ2IG5yVGIi/QTHcmhTRwejMkJISiraUFGZpHoOETkRKzeszcYDIiPj+/Z\n9vf3h16vh0Zz/eIqGRkZ+OCDD3q2c3Jy8Mwzz8BkMmH58uUYNmxYr5/j5+cBlYV7Tlqtl0XfT7Q9\nOWXIOVeLuEg//GjhCCiVt//t52htcKfu9fhfXJyI4ur92HOiAlPGhGN0rM5CyayH3wHnPn6AbQDY\nXxsIv4n6ZtOJ5ubmYuDAgT0/AEaOHAl/f39MmzYNubm5WL58ObZs2dLr+zY0WHblMa3WC3p9s0Xf\nU6Tq+jb87/rTcHdV4d/mxKG+vvW2+zhaG9wpSx3/M6lD8drHx/G7f5zAfz8zARp3Fwuksw5+B5z7\n+AG2AWC7bdDbDxCrn8bX6XQwGAw927W1tdBqr7/Na9++fZg4cWLPdkxMDKZNmwYAGD16NOrr69Hd\n3W2VvI6o22zG2i0F6DR24+nZsQj0dRcdyalEBnth4eRoXG3pwse7LnD+fCLqd1Yv9ikpKdi1axcA\noKCgADqd7oZT+GfOnEFcXFzP9tq1a7F161YAQGFhIfz9/aFU8uKmu7X9SBmKrzRjYnwQxg8NEh3H\nKc2ZEInBYT44fr4W2QXVouMQkYOz+mn8xMRExMfHIz09HZIkYdWqVVi/fj28vLwwc+ZMAIBer0dA\nQEDPPvPnz8fLL7+Mzz//HCaTCa+//rq1YzuMsppmbM4qhq9GjcdnDhEdx2kpFBJ+NG8YVn2Qg093\nF2JImC/PsBBRv5FkBz2HaOnxFFsdo7kTpm4z/vvD46jQt+AXj43EiIEBt9/pOxyhDe5Ffxz/oTNX\n8P62cxga6Ydl6aMgSbY9xwG/A859/ADbALDdNrCpMXsSZ/OhElToWzBlZMgdF3rqH8nDgzFqUCDO\nlTZgf16V6DhE5KBY7J1E8ZUmbM8uRYC3GxZN5/r0tkKSJDw5Kxburir88+tLqG/qEB2JiBwQi70T\nMHWb8cG2czDLMv4tNY7L1toYPy9XpN8/CB1d3fhoJ6/OJyLLY7F3AtuPlKLS0IppowZgaJS/6Dh0\nE5NGhCA+2h9nLtfhcD6vziciy2Kxd3BX6lqx9XAJfDRqPDptkOg4dAuSJOHp2bFwVSvx2Z6LuNrS\nKToSETkQFnsHZpZlfLTjPEzdMpbMjIWHG0/f27JAH3c8Ni0GbZ0m/P2rQtFxiMiBsNg7sAN5VSis\naETiEC3GxGpvvwMJN3V0KAaH+eDEBT3yLhluvwMRUR+w2Duoqy2dyMgsgrurEk9w8hy7oZAkPDUr\nFkqFhE93F6Kzi9NCE9G9Y7F3UP/4qhDtnSakTRsEPy9X0XHoDoRqNZg1PgJ1TR3YfKhYdBwicgAs\n9g4o/3Idjl/QY1CYD6aMGiA6Dt2F+SlRCPRxw+5j5aiobREdh4jsHIu9gzGauvHpV4VQSBKefCAW\nChuffpVuztVFiSUPDEG3WcbHuy7AzHvviegesNg7mB1Hy1Db0I77x4QhXKe5/Q5ksxJiAjE2VotL\nlY04yKl0iegesNg7EP3VdmzLLoWPRo2Fk6NFxyELWDxjCNzUSnyxrwgt7UbRcYjITrHYO5B/fFUI\no8mMRdMHcUpcB+Hn5YoFKdFo7TBhw4HLouMQkZ1isXcQuRf1yCuqQ1yELyYMDRIdhyxoxtgwhAR4\nYN+pSpTV2N6ymkRk+1jsHYDR1I3P9lyEUiFhyQOxNr8mOt0ZlVKBx2cMgSwDf/+qkAvlENEdY7F3\nALtyymFo7MCMsWEYEOgpOg71g/hofyQO0eJiRSOOnK0RHYeI7AyLvZ1raO7EtuxSeHu4YH4yL8pz\nZOnTB8FFpcA/My+hvdMkOg4R2REWezv3xb4idBq78fDUGC504+ACfd0xZ0IEGlu6sPVwieg4RGRH\nWOztWFFVI7ILqhERpMGkESGi45AVpCZFIsD72sx6tQ1touMQkZ1gsbdTZlnGZ3suAgAenzEECgUv\nynMGahcl0u6LQbdZRkZmkeg4RGQnWOzt1JGCalyuasL4oToMCfcVHYesaFycDoNCfXCiUI8LZQ2i\n4xCRHWCxt0OdXd34Yl8RXFQKpE0bJDoOWZkkSUi/fzAA4PO9lzhvPhHdFou9HdqVU4arLV2YNT4C\nAT5uouOQAAMHeCMpPgilNc3Izq8WHYeIbByLvZ1pbOnEjqNl8PZwwZwJEaLjkECPTImBi0qBL/cX\nobOrW3QcIrJhVr9Xa/Xq1cjLy4MkSVi5ciUSEhJ6nps+fTqCg4OhVCoBAG+//TaCgoJ63cfZbMwq\nRqexm/PfEwJ83DBrfAS2Hi7BjqOlWDh5oOhIRGSjrFotcnJyUFpainXr1qGoqAgrV67EunXrrnvN\n2rVr4enpeUf7OItKfQsO5FUhJMADk0fyVjsCUpMicPB0FXYeLcPUUaHw83IVHYmIbJBVT+NnZ2dj\nxowZAICYmBg0NjaipaXF4vs4qox9RZBl4LH7BkGp4AgMAW5qFRZOikaXyYxNWcWi4xCRjbJqz95g\nMCA+Pr5n29/fH3q9HhqNpuexVatWobKyEmPGjMHSpUv7tM/N+Pl5QKVSWjS/Vutl0fe7E6cKa3G6\nqA4JgwJxf1KUsMVuRLaBLbDF439o+hDsPVmJrNNVSJ8Vh/Cg/s1oi21gTc5+/ADbALC/NhA66Pv9\n1btefPFFTJ48GT4+Pnj++eexa9eu2+5zKw0Wnl1Mq/WCXi9meVGzLGPthjMAgIcmRcNgEHNmQ2Qb\n2AJbPv6HJkXjnfVnsHbDafzskf67psWW28AanP34AbYBYLtt0NsPEKueC9bpdDAYDD3btbW10Gq1\nPdsLFy5EQEAAVCoVpkyZgsLCwtvu4wyOFtSgrLYFE+ODEBlsX78myTpGDQ7E4DAf5F40oLD8qug4\nRGRjrFrsU1JSenrrBQUF0Ol0Pafjm5ub8cwzz6CrqwsAcOzYMQwePLjXfZyB0WTGhoOXoVJKeIhX\nW9MtSJKEtPuuTbCUkXmJa94T0XWseho/MTER8fHxSE9PhyRJWLVqFdavXw8vLy/MnDkTU6ZMwaJF\ni+Dq6ophw4Zh9uzZkCTphn2cyf5TlT1r1Qf6uouOQzZsUKgPxgzR4kShHicL9RgTqxMdiYhshCQ7\naBfA0uMpIsZo2jtNWPHXbBhNZrz53ER4e6it+vnfZ6vjVNZiD8d/pa4V//F/OdD6ueO1H423+F0b\n9tAG/cnZjx9gGwC22wY2M2ZPd2b3sXI0txkxe3yE8EJP9iEkwBNTRg1ATX0bDp3hNLpEdA2LvY1q\nau3Czpxr0+I+MD5cdByyI/OTo6BWKbApqxhGE6fRJSIWe5u19XAJOru6MT8lGm5qTotLfefn5Yr7\nx4ShobkTmScrRcchIhvAYm+DDFfbkZlbiUAfN0wdNUB0HLJDc5Ii4e6qxNbsUrR3mkTHISLBWOxt\n0KZDxeg2y3ho8kColPxPRHdO4+6C2eMj0NJuxO5j5aLjEJFgrCQ25kpdKw7nV2NAoCcmDAsSHYfs\n2Mxx4fD2cMGunDI0t3WJjkNEArHY25hNWcWQZeChydFQKMTMf0+OwU2twtzkKHR0dWNbdqnoOEQk\nEIu9DSmraUbOuVpEBnshcYhzTQlM/WPaqFAEeLvh65OVqG/qEB2HiARhsbchGw9eW6L04SkDha1q\nR47FRaXAgklRMHWb2bsncmIs9jaiqKoRpy4ZMDjMB8Oj/UXHIQeSPDwYQX7uOJBXBcPVdtFxiEgA\nFnsbseHAZQDs1ZPlKRUKLJgUjW6zjM2HS0THISIBWOxtwIWyBpwtaUB8tD9iI/xExyEHNGFoEAYE\neuLwmWrUNLSJjkNEVsZiL5gsy9jwzVg9l7Cl/qJQSHhwUjTMsozNWcWi4xCRlbHYC3a+tAGF5VeR\nEBOAgQO8RcchBzYmVoswrQZHCmpQZWgVHYeIrIjFXiBZlrHhm17WwsnRgtOQo1NIEh6aHA0Z1+Zz\nICLnwWIvUEFJPS5VNGLUoEBEBbNXT/1v1OBARAV74dj5WpTXtoiOQ0RWwmIviCzL2PTNWP2Dk9ir\nJ+uQJKnnLNLmQ+zdEzkLFntBzlyuR1FVExKHaBEZ7CU6DjmREQMDEB3ijRMX9OzdEzkJFnsBZFnG\npqxr99WzV0/WJklSz/eOvXsi58BiL8DpojoUX2nG2FgtwnUa0XHICY0Y6M/ePZETYbG3smu9+mu9\nqQXs1ZMg7N0TORcWeys7c7keJdXXevVhWvbqSRz27omcB4u9Fcmy3NOLmp/CXj2Jxd49kfNgsbei\nguJ6XP7mCnyO1ZMtYO+eyDmw2FuJLMvY9E3vaUFKlNgwRN+41ruPAgBs4Yp4RA5LZe0PXL16NfLy\n8iBJElauXImEhISe544cOYLf/e53UCgUiI6Oxuuvv45jx47hpZdewuDBgwEAQ4YMwX/8x39YO/Y9\nO1vSgKLKJoweHIiIIN5XT7ZjxMAARAZ74cT5WlQaWhEa6Ck6EhFZmFWLfU5ODkpLS7Fu3ToUFRVh\n5cqVWLduXc/z//mf/4mPP/4YwcHBePHFF3Hw4EG4ublh/Pjx+NOf/mTNqBZ1fa+eY/VkWyRJwoKU\nKLzz5RlsO1yCf18QLzoSEVmYVU/jZ2dnY8aMGQCAmJgYNDY2oqXlX+OE69evR3BwMADA398fDQ0N\n1ozXb86XNvTMgc/Z8sgWjRoUiHCdBkfP1eBKHVfEI3I0Vu3ZGwwGxMf/q9fg7+8PvV4PjebaxWrf\n/n9tbS0OHTqEl156CYWFhbh06RKee+45NDY24oUXXkBKSsptP8vPzwMqldKi+bXauyvUv8vIAwA8\nNW/YXb+HrbD3/PfKkY//iTlD8eZHx7A3twq/WJx4y9c5chv0hbMfP8A2AOyvDaw+Zv9dsizf8Fhd\nXR2ee+45rFq1Cn5+foiKisILL7yAOXPmoLy8HE899RR2794NtVrd63s3NLRZNKtW6wW9vvmO9yss\nv4r8ojqMGBgAXzfVXb2HrbjbNnAUjn78g4I1CA30xL4TFXhgTCh0fh43vMbR2+B2nP34AbYBYLtt\n0NsPEKuextfpdDAYDD3btbW10Gq1PdstLS149tln8fOf/xyTJk0CAAQFBSE1NRWSJCEiIgKBgYGo\nqamxZux78u0VzvOTo4TmILodhSRhXnIUzLKMbdmlouMQkQVZtdinpKRg165dAICCggLodLqeU/cA\n8Oabb+Lpp5/GlClTeh7bvHkz3n//fQCAXq9HXV0dgoKCrBn7rl2uakJBcT2GRvphUJiP6DhEtzUu\nTodgfw8czq+G4Wq76DhEZCFWPY2fmJiI+Ph4pKenQ5IkrFq1CuvXr4eXlxcmTZqEjRs3orS0FF98\n8QUAYN68eZg7dy6WLVuGvXv3wmg04tVXX73tKXxbsZW9erIzCoWE+clRWLv1LHYcLcOTs2JFRyIi\nC7D6mP2yZcuu246Li+v5c35+/k33+d///d9+zdQfymqaceqSAYPCfBAb4Ss6DlGfjR+mw8asyzh4\nugrzkqPg5+UqOhIR3SPOoNdPvh2rX5AcBUmSxIYhugNKhQJzJ0bB1C1jV06Z6DhEZAEs9v2gUt+C\nExf0iA7xQny0v+g4RHcseXgw/LxcsS+3Ek1tXaLjENE9YrHvB99eyTyPvXqyUyqlAqlJkegymfHV\nsXLRcYjoHrHYW1hNQxuOnqtBuE6DUYMCRcchumuTE0Lg7anG3hMVaO0wio5DRPeAxd7CdhwphSwD\ncydGsldPdk3tosTs8RHo6OrG3uMVouMQ0T1gsbeg+qYOHDpTjWB/D4yN1YmOQ3TPpo0eAE83Fb46\nXo72TpPoOER0l1jsLWjH0TJ0m2WkJkVCoWCvnuyfm1qFB8aFo7XDhH25laLjENFdYrG3kMbWLhzI\nq0KAtxuS4u1jhj+ivrh/TBjcXZXYdawcncZu0XGI6C6w2FvI7mNlMJrMSE2KgErJZiXH4eHmgumJ\nYWhq7cKeo5wzn8gesSpZQEu7EV+frISPRo1JCSGi4xBZ3Myx4VCrFPhy3yWYus2i4xDRHWKxt4C9\nJyrQ2dWNWeMi4KJSio5DZHHenmpMGTkA+oZ2ZBdUi45DRHeIxf4etXeasOd4OTzdVJg2eoDoOET9\nZvaECKiUErYfKYPZLIuOQ0R3gMX+Hu0/VYXWDhNmjguHm9rq6woRWY2/txumj41ATX0bjl+oFR2H\niO4Ai/09MJq6setYGVzVStw/Jkx0HKJ+98j0QZCka1NCyzJ790T2gsX+HmSdqUZjSxemjw6Fp5uL\n6DhE/W5AoAbjhwahvLYFp4vqRMchoj7q9bxzeXnvC2CEh4dbNIw96TabseNIKVxUCjwwPkJ0HCKr\nmZsUiaNna7A1uwQJMQGcFprIDvRa7J9++mlIknTT03WSJGHv3r39FszW5ZythaGxA9MTQ+HjqRYd\nh8hqwr5Z5OnUJQMKy68iNsJPdCQiuo1ei/3XX399y+dOnDhh8TD2wizL2HakFEqFhNkT2Ksn5zN3\nYiROXTJga3Ypiz2RHejT5eMtLS3YtGkTGhoaAABGoxFffvklsrKy+jWcrTp10YAqQytShgcj0Mdd\ndBwiq4sJ9cHQSD8UFNej+EoTokO8RUciol706QK9n//857hw4QLWr1+P1tZWZGZm4tVXX+3naLZJ\nlmVsyy6BBGBOUqToOETCpE689v3fns0pdIlsXZ+KfWdnJ/77v/8boaGhWL58OT7++GPs2LGjv7PZ\npLOlDSi+0ozEIVoMCPQUHYdImGGRfogO8cKJQj0qDa2i4xBRL/pU7I1GI9ra2mA2m9HQ0ABfX9/b\nXqnvqLYdLgEAzE1mr56cmyRJmDsxCgCw4wh790S2rE/F/sEHH8Q///lPpKWlITU1FXPnzkVAQEB/\nZ7M5lyobcb7sKuKj/REVzDFKolGDAzEg0BNHCmpguNouOg4R3UKfLtBbvHhxz58nTpyIuro6DBs2\nrN9C2apvxybnTWSvnggAFJKEuUmRWLv1LHYcLcOTs2JFRyKim+hTsdfr9di+fTsaGxt77rn/6quv\n8NJLL/VrOFtScqUJpy4ZMCjUB0PCfUXHIbIZ44fpsOHgZRw8fQULUqLgo3EVHYmIvqdPp/F//OMf\n4/z581AoFFAqlT3/uxurV6/GokWLkJ6ejtOnT1/33OHDh/Hoo49i0aJFePfdd/u0j7V8sfcigGtX\nIHPGMKJ/USoUmDMhAqZuM3Yfd85reYhsXZ969h4eHnjjjTfu+cNycnJQWlqKdevWoaioCCtXrsS6\ndet6nn/ttdfw/vvvIygoCEuWLMGsWbNQX1/f6z7WUNvQhoOnKhCm1WBkjPNdq0B0O5MSQrDpUAky\nT1ZiblIkPLhWBJFN6VPPfuQ/tKLqAAAgAElEQVTIkSgqKrrnD8vOzsaMGTMAADExMWhsbERLSwuA\na/Pw+/j4ICQkBAqFAlOnTkV2dnav+1jLjqNlMMvXZg1jr57oRi4qJWaND0dHVzf2nqwUHYfIpsmy\njAN5VSitbrbaZ/apZ3/w4EF8+OGH8PPzg0qlgizLkCQJ+/btu6MPMxgMiI+P79n29/eHXq+HRqOB\nXq+Hv7//dc+Vl5ejoaHhlvv0xs/PAyrV3Q013JC7qRMRwV6YM2kglErnXihQq/USHUEoZz9+4NZt\n8OiMWGw/Uoa9Jyrw+OyhcHPt0z8vdoffAbYBcG9tcPqSHh/uOI85E6MwdsQAC6a6tT79bfzLX/7S\nLx9+N+th93Wfhoa2O37vW/npg/HQar1QX+/cE4dotV7Q6633S9TWOPvxA7dvg+mjQ7HlcAnW7y3E\nzHGOtyomvwNsA+De2+AfO84BAMYMDrRoW/b2A6RPxT47O/vGHVUqGAwGjBw5ss9BdDodDAZDz3Zt\nbS20Wu1Nn6upqYFOp4OLi8st97EWd1cV3F1VsO7gAZH9mTE2DLuOlWFnThnuSwyFysnPhBF9X/GV\nJhSUNGBopB8GDrDefC19+pt46NAh/Pa3v8WePXuQmZmJt99+G3v37sWqVavw+9//vs8flpKSgl27\ndgEACgoKoNPpek7Hh4WFoaWlBRUVFTCZTMjMzERKSkqv+xCRbfHyUGPaqFA0NHciO79adBwim7NN\n0HwtferZd3d3Y/v27QgMDAQA1NXV4Y033sCGDRuQnp7e5w9LTExEfHw80tPTIUkSVq1ahfXr18PL\nywszZ87Eq6++iqVLlwIAUlNTER0djejo6Bv2ISLbNWt8BPaeqMD2I6VIGREChYIXtRIBQKWhFScL\n9YgO8UZcpHWXhu5Tsa+pqekp9AAQEBCAiooKSJIEs9l8Rx+4bNmy67bj4uJ6/jxu3Lib3lb3/X2I\nyHb5ebkiZUQwDuRdwfELtRg/NEh0JCKb8O0aEvME3NnVp2I/YMAAvPjiixg/fjwkSUJubi48PT2x\nc+dOhISE9HdGIrIzcyZE4uDpK9iWXYpxcTreskpOz3C1HUcKahAa6ImRgwNvv4OF9anYv/XWW9i0\naRPOnz8Ps9mMkSNH4qGHHkJrayumTp3a3xmJyM4E+XtgXJwOOedqceZyHRJirP+PG5Et2ZlTBrMs\nIzUpEgoBP357Lfa1tbXQ6XSoqalBUlISkpKSep5raGhAeLjj3VpDRJYxd2IUcs7VYuvhUowYGMDe\nPTmtxtYuHDx9BYE+bhg/TCckQ6/F/q233sKaNWvw9NNP3/Qv6t69e/stGBHZt3Ddteml84rqUFh+\nFbER1r0gichW7D5WBqPJjDkTIqBUiLkdtddPffXVV/Hhhx/i66+/xt69e/GjH/0Inp6eiI+Pt/r8\n9ERkf+YmRwH41+1GRM6mtcOIzJOV8PFUY1KCuGvcei32q1atQl1dHQCguLgYv//97/GrX/0KKSkp\neP31160SkIjs16BQH8RF+CK/uB4l1U2i4xBZ3d4TFejo6sas8RFwsdAU7nej12JfXl7ec9/7rl27\nMHv2bEycOBGLFi26blY7IqJbmTsxCgCw7TB79+RcOrpM+OpYOTzdVJg22jpz4N9Kr8Xew8Oj5885\nOTnXXaDHi22IqC+GRfkhKtgLJwv1qDI49/oS5Fz2n6pCa4cJM8aGw00tdmGoXot9d3c36urqUFZW\nhtzcXKSkpAAAWltb0d7ebpWARGTfJEnC3IlRkAFsP8LePTkHo8mMnTllcFUrcf+YMNFxei/2zz77\nLFJTUzF//nz89Kc/hY+PDzo6OvD4449j4cKF1spIRHZu9JBADAj0xJGCGuivsqNAju9Q/hU0tnTh\nvtGh0Li7iI7T+613U6dORVZWFjo7O3sWn3Fzc8PLL7+MSZMmWSUgEdk/hSRhblIk1m49i51Hy/Dk\nrFjRkYj6TbfZjB1HSqFSKvCAjSz1fNsb/lxcXG5YZY6Fnoju1PhhOmh93XDw9BVcbekUHYeo3+Sc\nrYX+agcmJYTAV+MqOg6APi5xS0R0r5QKBeYkRcLUbcbunHLRcYj6hVmWse1IKRSShNQJEaLj9GCx\nJyKrSRkeAl+NGpm5lWhpN4qOQ2Rxud/cdTIxPgiBvu6i4/RgsSciq3FRKTB7fAQ6jd3Yc5y9e3Is\nsixj6+FSSABSJ0aKjnMdFnsisqqpo65dnbzneAXaO02i4xBZTH5xPUprmjEmToeQAE/Rca7DYk9E\nVuWqVmLmuHC0dZqQmVspOg6RRciyjC2HSwAA82ysVw+w2BORAPcnhsHdVYVdOWXoNHaLjkN0zwrL\nr+JSRSMSYgIQEeQlOs4NWOyJyOo83FS4f0wYmtuMOHCqSnQconu29dte/TcrPdoaFnsiEmLm2DC4\nuiix42gpjCaz6DhEd634ShMKShowNNIPg0J9RMe5KRZ7IhLCy0ON+0aH4mpLFw6duSI6DtFd23Ko\nBIBtjtV/i8WeiISZNT4cKqUC24+UwtTN3j3Zn7KaZpy6ZMCgUB/ERfqJjnNLLPZEJIyPxhVTRw6A\nobEDR8/WiI5DdMe+O1Zvy0u/s9gTkVCzJ0RAqZCwNbsUZrMsOg5Rn1UaWnHigh5RwV4YMdBfdJxe\nsdgTkVABPm5IGRGMmvo2HDtfKzoOUZ9tyy6BDGC+jffqgdsscWtpRqMRK1asQFVVFZRKJd544w2E\nh1+//N/27dvxwQcfQKFQYOLEifjFL36B9evX449//CMiIq4tKpCcnIyf/OQn1oxORP0oNSkSWaer\nsfVwCcYN1UFh4/9wEtXUt+Ho2RqEaTUYOThQdJzbsmqx37p1K7y9vbFmzRpkZWVhzZo1+MMf/tDz\nfHt7O95++21s3rwZnp6eeOyxxzB//nwAQGpqKpYvX27NuERkJTo/DyTFB+FwfjVOXtBjbJxOdCSi\nXm3LLoUsA/OSI+3ix6lVT+NnZ2dj5syZAK71zk+ePHnd8+7u7ti8eTM0Gg0kSYKvry+uXr1qzYhE\nJMjciZGQJGDL4RLIMsfuyXbV1Lchu6AaIQEeGBtrHz9MrdqzNxgM8Pe/dhGDQqGAJEno6uqCWq3u\neY1GowEAXLhwAZWVlRg5ciTKysqQk5ODZ555BiaTCcuXL8ewYcN6/Sw/Pw+oVEqL5tdqbW8KRGtz\n9jZw9uMH+q8NtFovTBkVhv25FSiubcWE4SH98jn3it8BtsG7X+Sh2ywj/YE4BAV5i47TJ/1W7DMy\nMpCRkXHdY3l5eddt3+rXe0lJCZYtW4Y1a9bAxcUFI0eOhL+/P6ZNm4bc3FwsX74cW7Zs6fXzGxra\n7u0Avker9YJe32zR97Q3zt4Gzn78QP+3wYwxoTiQW4FPdpxDtM7T5i564neAbVDX2IE9OaXQ+blj\nWLi3TbVFbz/C+q3Yp6WlIS0t7brHVqxYAb1ej7i4OBiNRsiyfF2vHgCqq6vx/PPP47e//S2GDh0K\nAIiJiUFMTAwAYPTo0aivr0d3dzeUSsv23IlIrNBAT4yJ0+H4+VqcuVyPhJgA0ZGIrnNtAigZ85Oj\noFTYzw1tVk2akpKCnTt3AgAyMzMxYcKEG17zyiuv4NVXX0V8fHzPY2vXrsXWrVsBAIWFhfD392eh\nJ3JQ879ZSGTzoWKO3ZNNqW/qwMHTVQgJ8ERSfJDoOHfEqmP2qampOHz4MBYvXgy1Wo0333wTAPDe\ne+9h3Lhx8PX1xfHjx/GnP/2pZ58f/OAHmD9/Pl5++WV8/vnnMJlMeP31160Zm4isKFynwejBgci9\naEBBST2GR7N3T7Zhx5EymLplPDZjsF316gErF/tv763/vn//93/v+fP3x/W/9cknn/RbLiKyLQtS\nopF70YDNWSWIj/K3ubF7cj4NzZ3Yn1eFQB83TBsTjob6VtGR7oh9/TQhIqcQGeyF0YMDcamyEWdL\nGkTHIcKObxZrmpccBZXS/kqn/SUmIqewICUaALApi2P3JNbVlmu9+gBvNyQPDxYd566w2BORTfpu\n776gpF50HHJiO46UwWgyY25ypF326gEWeyKyYezdk2gNzZ3IzK1EgLcrJo2wzYme+oLFnohs1re9\n+6LKJvbuSYjt34zVz0+JtttePcBiT0Q2jr17EqW+qQP7T1Ui0Md+x+q/xWJPRDbtut59MXv3ZD3b\nvp0tL8U+r8D/LvtOT0RO4cFJ13r3Gw5eZu+erKKusQMHTlVB5+tu9716gMWeiOxARJAXxsZqUXyl\nGXmX6kTHISewLbsE3eZrvXp7my3vZuz/CIjIKTw4KRoSgI0HL8PM3j31I8PVdhw8fQVBfu52Nwf+\nrbDYE5FdCNVqMGFYEMpqW3Dygl50HHJgWw5f69UvmBTtEL16gMWeiOzIgknRkKRrV+abzezdk+XV\n1Lfh0JlqhAR4YMJQx+jVAyz2RGRHgv09kDw8GJWGVuScrxEdhxzQpqximGUZD00eCIXCcRZgYrEn\nIruyICUaSoWETVkl6DabRcchB1Khb8HRszWI0GmQGKsVHceiWOyJyK5ofd0xKSEENfVtyM5n754s\nZ8OBy5ABPDRlIBQOtqwyiz0R2Z353ywzuimrGEYTe/d074qvNCH3ogExod5IiAkQHcfiWOyJyO74\ne7themIo6r6ZzpToXm04cBkA8PCUGEgO1qsHWOyJyE6lToyEq1qJrYdL0NFlEh2H7Fhh+VXkF9dj\naKQfhkb6iY7TL1jsicgueXuoMWtcOJrajNhzvEJ0HLJTsizjy/1FAICHpwwUnKb/sNgTkd2aNT4C\nGncX7DhahpZ2o+g4ZIfyiupwsaIRowYFIibUR3ScfsNiT0R2y91VhdSkSLR3mrDjaKnoOGRnzOZr\nvXoJwMNTHbdXD7DYE5Gdm54YCj8vV+w9XoGrLZ2i45AdOXK2GpX6ViQPD0aYViM6Tr9isSciu6Z2\nUWJ+ShS6TGZsPlQiOg7ZCaPJjA0HiqFSSnhwcrToOP2OxZ6I7N7khBAE+XvgwKkqXKlrFR2H7MC+\n3ErUNXVgemIYAn3cRcfpdyz2RGT3lAoFHp06EGZZxvr9l0XHIRvX3mnClsMlcFMrMXdipOg4VqGy\n5ocZjUasWLECVVVVUCqVeOONNxAeHn7da+Lj45GYmNiz/eGHH8JsNt92PyJybolDtIgJ9caJQj0u\nVTZikANfWU33ZlfOtbs3Fk6OhpeHWnQcq7Bqz37r1q3w9vbGZ599hueeew5r1qy54TUajQaffPJJ\nz/+USmWf9iMi5yZJEh67bxAA4J+ZlyDLXAKXbnS1pRO7csrh7anGA+Ocp9No1WKfnZ2NmTNnAgCS\nk5Nx8uTJft2PiJzL4DBfjB4ciEsVjTh10SA6DtmgjQeL0WnsxsJJ0XBTW/XktlBWPVKDwQB/f38A\ngEKhgCRJ6Orqglr9r9MoXV1dWLp0KSorKzFr1iz88Ic/7NN+3+fn5wGVSmnR/Fqtl0Xfzx45exs4\n+/EDtt8Gzz6UgBfezsSGrGLcnxQFpdKyfRpbP35rsNc2KK1uQtbpKoQHafDw/UPu6bthb23Qb8U+\nIyMDGRkZ1z2Wl5d33fbNTrP98pe/xIIFCyBJEpYsWYKxY8fe8Jq+nJ5raGi7w8S902q9oNc3W/Q9\n7Y2zt4GzHz9gH23gprh2df7+U1VY/3Uhpo0Ktdh728Px9zd7boP31p+GWQYenjwQ9fV3f9eGrbZB\nbz9A+q3Yp6WlIS0t7brHVqxYAb1ej7i4OBiNRsiyfEPvfPHixT1/TkpKQmFhIXQ63W33IyL61oOT\nonGkoAYbDxZjwtAguLs6z+laurmCknqcLqpDXISvQy5heztWHbNPSUnBzp07AQCZmZmYMGHCdc9f\nvnwZS5cuhSzLMJlMOHnyJAYPHnzb/YiIvstX44o5SRFoau3C9iOcRtfZmWUZGV9fAgAsmj7YIZew\nvR2r/txNTU3F4cOHsXjxYqjVarz55psAgPfeew/jxo3D6NGjERwcjEcffRQKhQLTp09HQkIC4uPj\nb7ofEdGtzBofgf2nqrArpxxTRw5AoK/jT5xCN5edX42y2hZMjA9GZLB9jbVbiiQ76P0plh5PsdUx\nGmty9jZw9uMH7K8NsvOrsXbrWYwfqsNzDw6/5/ezt+PvD/bWBp1d3Vi59gha2o1Y/WwSAnzc7vk9\nbbUNehuz5wx6ROSwJsQHITrECznnanGpslF0HBJgx9FSNDR34oFx4RYp9PaKxZ6IHJZCkrBo+mAA\nwOd7L8LsmCcy6RbqGjuw42gZfDRqp5kW91ZY7InIoQ0J98XYOB0uVzUh52yN6DhkRRn7LsFoMiNt\nWoxTTaBzMyz2ROTw0qbFQKVUIGNfETq7ukXHISsoLL+KnHO1GDjAG0nxwaLjCMdiT0QOT+vrjtkT\nwtHQ3Imt2SWi41A/M5tl/GNPIQBg8YzBUDjhrXbfx2JPRE5hblIU/L1dsSunDDUWnmGTbEvWmSso\nq7l2q13MAK5+CLDYE5GTcFUr8dh9g2DqlvH5noui41A/aeswYv3+Iri6KPHotBjRcWwGiz0ROY1x\ncTrERfgir6gOp4u4Kp4j2nCgGE1tRsxLjoSfl6voODaDxZ6InIYkSXh85hAoJAn/2HMRRpNZdCSy\noNLqZnydW4Fgfw/MGh8hOo5NYbEnIqcSptVgemIoahvasftYmeg4ZCFmWcanuy9AloEnHhgClYWX\nNrZ3bA0icjoLJ0fDy8MFWw6XwNDYLjoOWUDW6SsoqmrCuDgd4qP8RcexOSz2ROR0PNxc8Nh9g9Bl\nNOPvuwvhoEuEOI2WdiO+2FcEV7US6fcPFh3HJrHYE5FTSh4e3HOx3slCveg4dA/W7y9CS7sRD6ZE\n86K8W2CxJyKnJEkSnpwVC5Xy2sV67Z0m0ZHoLhRVNmL/qSqEBnpixtgw0XFsFos9ETmtkABPpCZF\noqG5ExsPFouOQ3fI1G3GhzvOQwa++eHGknYrbBkicmpzJ0YiyM8de06Uo7Ta9tYop1vbcaQUlYZW\nTBs1AEPCfUXHsWks9kTk1FxUSiyZFQtZBj7aeR7dZt57bw+u1LViy+ES+GjUeHTaINFxbB6LPRE5\nvfgof0yMD0ZJdTN2HysXHYduwyzL+GjHeZi6ZSyZGQsPN+devrYvWOyJiHBtdTRvDxdsOFCMK3Wt\nouNQLw6cqkJhRSMSh2gxJlYrOo5dYLEnIgKgcXfBkgdiYeo24287zsPMe+9tUkNzJzL2XYK7qxJP\nzBwiOo7dYLEnIvrG2DgdxsZqcamiEV+fqBAdh75HlmV8uOM82ju7kTZtEO+pvwMs9kRE3/HEA7Hw\ndFPhi/1F0F/lVLq25ODpKzhzuQ7xUX6YOmqA6Dh2hcWeiOg7fDzVeHzmEHQZr93DzdP5tsHQ2I7P\n916Eu6sSP0wdCkmSREeyKyz2RETfkzQsCKMGBeJcaQP2HufpfNHMsoy/bT+Pjq5uLL5/CPy93URH\nsjss9kRE3yNJEp6eHQuNuwsy9hWhUt8iOpJTyzxZiXOlDRg1KBApI4JFx7FLVr050Wg0YsWKFaiq\nqoJSqcQbb7yB8PDwnufz8/Px1ltv9WxfunQJ7777Lg4dOoQtW7YgKCgIALBgwQKkpaVZMzoRORkf\njSt+OCcO76w/g7VbzuLXT48VHckp1TS0IWPfJXi6qfD07Fievr9LVi32W7duhbe3N9asWYOsrCys\nWbMGf/jDH3qeHz58OD755BMAQFNTE376059i1KhROHToEJ566iksWbLEmnGJyMmNHqLFlJEhOJB3\nBRsOXsZP00aLjuRUTN1m/HVTAbqMZvxb6lD4aHj1/d2y6mn87OxszJw5EwCQnJyMkydP3vK177//\nPp5++mkoFBxpICJx0u8fDJ2vO3YeKcOZIoPoOE5l/YHLKKluRsqIYIwfGiQ6jl2zas/eYDDA398f\nAKBQKCBJErq6uqBWq697XUdHB7KysvDSSy/1PLZz507s3bsXarUav/71r687/X8zfn4eUKmUFs2v\n1XpZ9P3skbO3gbMfP+CcbfDyU2Ox/H+y8PvPTuJP/28aNB7q2+/kwKzxHTh5oRY7j5ZhQKAnXlo8\nBu6utjUlrr39Pei31svIyEBGRsZ1j+Xl5V23Ld/ilpY9e/Zg2rRpPb36qVOnIikpCePGjcO2bdvw\n2muv4a9//Wuvn9/Q0HYP6W+k1XpBr3fuFbGcvQ2c/fgB522DAA8XzE+OwqasYvz242N44eERTjt2\nbI3vQGNrF9b8/QSUCgnPzhuGlqZ22NIlkrb696C3HyD9VuzT0tJuuIhuxYoV0Ov1iIuLg9FohCzL\nN/TqASAzMxOLFy/u2U5ISOj58/Tp0/H222/3V2wiopuanxyF4upm5F404Ktj5XhgfIToSA7JLMt4\nf9tZNLV2YdH0QYgMtq8etK2y6oB4SkoKdu7cCeBaQZ8wYcJNX5efn4+4uLie7ddeew3Hjx8HAOTk\n5GDw4MH9H5aI6DsUCgnLnhgDb081MvYVoaiyUXQkh7TraBnyL9dj+EB/zBzX+3At9Z1Vi31qairM\nZjMWL16Mv//971i6dCkA4L333kNubm7P65qamqDRaHq209LS8Pbbb2PJkiX4v//7P7zyyivWjE1E\nBADw83bDj+cPg1mW8ZdN+WhpN4qO5FDOldTji/1F8NWo8czcYVA46VBJf5DkWw2c2zlLj6fY6hiN\nNTl7Gzj78QNsg2+Pf3NWMTZmFSMhJgAvPprgVEWpv74D9U0dePVvx9DeacLyJxIxKNTH4p9hKbb6\n96C3MXve10ZEdIfmJUdhWJQfThfVYcuhEtFx7J7RZMa7G66dKVk8Y7BNF3p7xWJPRHSHFAoJP14Q\nj0AfN2zKKsbx87WiI9m1f+wpRPGVJiQPD8Z9o0NFx3FILPZERHfBy0ONnz2SAFcXJf5v21mU1dje\naV17sP9UJfafqkKEToOnZnE63P7CYk9EdJfCdRr8aN5QdBnNeOfLM2hq6xIdya4UlNTj092F0Li7\n4PmHR0DtYtmJ0OhfWOyJiO7BmFgdFk6KRl1TB/68IR+mbrPoSHah0tCKP2/IhyQBLzw8Alpfd9GR\nHBqLPRHRPZqXEoUxsVoUll/FB9vPweyYNzlZTGNrF/6YkYf2ThP+LXUohoT7io7k8FjsiYjukUKS\n8KN5wxAzwBtHCmrwRWaR6Eg2q8vYjXe+PA1DYwcWTopGUjzXp7cGFnsiIgtwdVHixUcTEOzvgZ05\nZdiVUyY6ks0xdZvx180FuFzVhInxwZifEiU6ktNgsScishAvDzX+36KR8NWose7rS8guqBYdyWaY\nzTI+2HYOuRcNGBrphx/MieOV91bEYk9EZEGBPu74f4+NgrurCh9sO4e8SwbRkYSTZRkf77qAI2dr\nMCjUBz97ZARcVCw/1sTWJiKysDCdBi8+MgJKhYR3N5zBKScu+LIsY93Xl3AgrwoRQRr8PC0Bbmrb\nWpveGbDYExH1g9gIP7z0zbz5764/g1MXna/gy7KM9QcuY/excgwI9MTSRaPg4eYiOpZTYrEnIuon\nQ6P88fO0kVAqr/Xwcy/qRUeyGrMs4x97LmJbdil0vu5YumgUvDzUomM5LRZ7IqJ+FBfph198U/D/\nvCEfx5xgHv1usxnvbz2HvScqEKr1xIolifDzchUdy6mx2BMR9bPYiGsFX6VS4H835mNXThkcdHVx\nGE3d+POGfGQXVGPgAG8sfzwRvhoWetFY7ImIrCA2wg+/eiIR3t/clvePPRdhNjtWwW9u68Lv1uX1\n3F63LH0UNO4co7cFLPZERFYSEeSFXz85FqGBnth7ogLvbjiDTmO36FgWUVbTjN98dBwXyq9iTKyW\nV93bGBZ7IiIrCvBxw6+WJGJopB9yLxrw+scnUF3fJjrWPTl+vharPz3RMwXuTxYOh4uKK9jZEhZ7\nIiIr83BzwS8eG4lpo0NRoW/Bf314DEfP1oiOdcdM3WZ8ub8If96YD0mS8MLDI7BgUjQUnBnP5vAc\nCxGRACqlAk/NikVsuC8+3Hkef91cgAvlV7H4/kF20Su+UteKtVvOoqS6GVpfN/zskQSEaTWiY9Et\nsNgTEQk0YVgQIoO98JeN+diXW4kLZQ3XfgRE+ImOdlNms4yvjpXji/1FMJrMSB4ejMdnDIGHG8uJ\nLeN/HSIiwYL9PfDKk2Pwxb4i7D1Rgbf+kYtJCSF47L5BNnU1e0VtC36fcRpnigzQuLvg3+cPw5hY\nnehY1Acs9kRENkDtosTjM4cgKT4YH+08j6zTV3DqogGPTB2IlBEhUCnFXWLV2NKJDQcv4+DpK5Bl\nYNSgQDw9Jw4+npwRz16w2BMR2ZCBA7zxnz8Yi6+OVWBj1mV8tPMCtmWXYu7ESKsX/ZZ2I74+UYEd\nR8vQaexGaKAnnn1oBCICPKyWgSyDxZ6IyMYoFQrMnhCBCcOCsP1IKfafqsJHOy9g6+ESPDA+AknD\ngvp1nvkKfQv2HK/AkYJqdJnM8PJwwaLpgzB5ZAiCg3yg1zf322dT/7B6sc/JycFLL72E1atX4777\n7rvh+c2bN+Ojjz6CQqHAY489hrS0NBiNRqxYsQJVVVVQKpV44403EB4ebu3oRERW5efliidmDkFq\nUiR2HL1W9D/bcxH//PoSEmICkDw8GAkxgRZZG97Q2I7TRXU4fr4W58uuAgACfdxw/5gwTBk5AO6u\n7BvaM6v+1ysrK8Pf/vY3JCYm3vT5trY2vPvuu/jiiy/g4uKCRx99FDNnzkRmZia8vb2xZs0aZGVl\nYc2aNfjDH/5gzehERML4ebni8RlDMHdiFLLzq5FdUI3ciwbkXjRA7aJAzAAfDA7zwaAwH0SHeMPD\nVQWpl3vdTd1m1DS044qhFSXVzThdZECFvrXn+aGRfpgxNgwjYwKhUPCeeUdg1WKv1WrxP//zP3jl\nlVdu+nxeXh5GjBgBLy8vAEBiYiJOnjyJ7OxsLFy4EACQnJyMlStXWi0zEZGt8PFUY/aECMyeEIHy\n2hZk51fjTHEdzpU24GsiaxQAAAgdSURBVFxpQ8/r1C4K+Gpc4euphqe7C7rNMrqM3TCazGjrNKG2\noR3d35mXX6VUYMTAAIwaFICEmEAE+LiJODzqR1Yt9u7u7r0+bzAY4O/v37Pt7+8PvV5/3eMKhQKS\nJKGrqwtq9a3HrPz8PKCy8MQUWq2XRd/PHjl7Gzj78QNsA1s5fq3WC4nxIQCuLUBzvqQeZ4vrUXKl\nCfVNHahv6sDFykZ8d3E9pUKCm6sKg8J8ER7khYhgL0QGe2NYtD/c7uA0va20gUj21gb9VuwzMjKQ\nkZFx3WM/+9nPMHny5D6/x62WgOzL0pANDZada1qr9XL6i1KcvQ2c/fgBtoEtH3+U1hNRWs/rHjN1\nm9HR1Q2VUoKLSgGl4uZj+81N7ejrUdlyG1iLrbZBbz9A+q3Yp6WlIS0t7Y720el0MBgMPdu1tbUY\nNWoUdDod9Ho94uLiYDQaIctyr716IiK6dnpe484lUMjGFsIZOXIkzpw5g6amJrS2tuLkyZMYO3Ys\nUlJSsHPnTgBAZmYmJkyYIDgpERGR/bDqmP2+ffvw/vvv4/LlyygoKMAnn3yCDz74AO+99x7GjRuH\n0aNHY+nSpXjmmWcgSRKef/55eHl5ITU1FYcPH8bixYuhVqvx5ptvWjM2ERGRXZPkvgyA2yFLj6fY\n6hiNNTl7Gzj78QNsA2c/foBtANhuG/Q2Zm9Tp/GJiIjI8ljsiYiIHByLPRERkYNjsSciInJwLPZE\nREQOjsWeiIjIwbHYExEROTgWeyIiIgfnsJPqEBER0TXs2RMRETk4FnsiIiIHx2JPRETk4FjsiYiI\nHByLPRERkYNjsSciInJwLPZ9sHr1aixatAjp6ek4ffq06DhCFBYWYsaMGfj0009FRxHit7/9LRYt\nWoRHHnkEu3fvFh3Hqtrb2/HSSy9hyZIlSEtLQ2ZmpuhIwnR0dGDGjBlYv3696ChWd/ToUSQlJeHJ\nJ5/Ek08+id/85jeiI1nd5s2bsWDBAjz88MPYt2+f6Dj/v727CYlqjeM4/rUZhrDpzakZC5KgVSTR\nKEWWREUUtWthTLMQCoJRWlQYhIpNRItxFU1iMdWiRTiiEEGFUSpFjEW7rBYpEb704uQU1tFS4y7i\nyr1wuWY3znOd8/usZmb13Zz5z/OcOefMiNt0wP/d48ePef36Nclkkt7eXqqrq0kmk6azbGVZFqdP\nn6akpMR0ihFdXV28fPmSZDJJJpNh79697Ny503SWbTo6OigsLOTQoUMMDAxw8OBBtm3bZjrLiMbG\nRhYuXGg6w5gNGzZw7tw50xlGZDIZGhoaaG1txbIs4vE4W7duNZ310zTsp5FKpdixYwcAq1at4tOn\nT3z+/Bmv12u4zD4ej4dEIkEikTCdYsT69etZu3YtAAsWLGB0dJTJyUlcLpfhMnvs2bNn6vWbN28I\nBAIGa8zp7e2lp6dnVn3By++TSqUoKSnB6/Xi9Xpn3c6GtvGnkU6nWbx48dT7vLw8hoaGDBbZz+12\nM3fuXNMZxrhcLnJzcwFoaWlhy5Ytjhn0fxUKhaiqqqK6utp0ihGxWIwTJ06YzjCqp6eHSCTC/v37\nefjwoekcW/X39zM2NkYkEiEcDpNKpUwnzYhW9jOkuws71927d2lpaeHKlSumU4xoamrixYsXHD9+\nnBs3bpCTk2M6yTbXr19n3bp1rFixwnSKMStXruTw4cPs3r2bvr4+ysvLuXPnDh6Px3SabT5+/Mj5\n8+cZHBykvLycjo6OWXMcaNhPw+/3k06np96/f/+epUuXGiwSEx48eMCFCxe4dOkS8+fPN51jq+7u\nbnw+H8uWLWP16tVMTk4yPDyMz+cznWabzs5O+vr66Ozs5O3bt3g8HvLz89m0aZPpNNsEAoGpUzoF\nBQUsWbKEd+/eOeYHkM/nIxgM4na7KSgoYN68ebPqONA2/jQ2b95MW1sbAM+ePcPv9zvqfL3AyMgI\n9fX1XLx4kUWLFpnOsd2TJ0+mdjPS6TSWZf3t1JYTnD17ltbWVpqbmykrK6OystJRgx5+/BP98uXL\nAAwNDfHhwwdH/X+jtLSUrq4uvn//TiaTmXXHgVb20ygqKmLNmjWEQiFycnI4efKk6STbdXd3E4vF\nGBgYwO1209bWRjwed8zgu3XrFplMhiNHjkx9FovFWL58ucEq+4RCIWpqagiHw4yNjVFXV8ecOVon\nOM327dupqqri3r17jI+PE41GHbWFHwgE2LVrF/v27QOgtrZ2Vh0HesStiIhIlps9P0tERETkl2jY\ni4iIZDkNexERkSynYS8iIpLlNOxFRESynC69E5F/VV9fz9OnT/n69SvPnz8nGAwCsHHjRvx+P2Vl\nZYYLRWQ6uvRORH5Kf38/4XCY+/fvm04RkRnSyl5Efkk8HmdiYoKjR48SDAapqKigvb2d8fFxIpEI\nzc3NvHr1img0SmlpKYODg5w6dYrR0VEsy+LYsWOOuwudiCk6Zy8i/5llWRQWFtLU1ERubi7t7e0k\nEgkqKyu5du0aANFolAMHDnD16lUaGxupra1lYmLCcLmIM2hlLyK/RXFxMfDjtqJFRUUA5OfnMzIy\nAsCjR4/48uULDQ0NwI9HJzvt/uoipmjYi8hv4XK5/vH1nzweD/F4nLy8PDuzRARt44uITYqLi7l9\n+zYAw8PDnDlzxnCRiHNoZS8itqipqaGuro6bN2/y7ds3KioqTCeJOIYuvRMREcly2sYXERHJchr2\nIiIiWU7DXkREJMtp2IuIiGQ5DXsREZEsp2EvIiKS5TTsRUREspyGvYiISJb7A9poO8yMDF/HAAAA\nAElFTkSuQmCC\n",
90 | "text/plain": [
91 | ""
92 | ]
93 | },
94 | "metadata": {
95 | "tags": []
96 | }
97 | }
98 | ]
99 | }
100 | ]
101 | }
--------------------------------------------------------------------------------
/NoteBooks/Recon_Example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "name": "Recon_Example.ipynb",
7 | "provenance": [],
8 | "include_colab_link": true
9 | },
10 | "kernelspec": {
11 | "name": "python3",
12 | "display_name": "Python 3"
13 | }
14 | },
15 | "cells": [
16 | {
17 | "cell_type": "markdown",
18 | "metadata": {
19 | "id": "view-in-github",
20 | "colab_type": "text"
21 | },
22 | "source": [
23 | "
"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {
29 | "id": "WxIjqKUIfTgl"
30 | },
31 | "source": [
32 | "# MRI Reconstruction Excercise\n",
33 | "\n",
34 | "This Jupyter notebook provides some hands on experience with raw data from an MRI scan. Each code cell can be run by clicking on the upper left corner. You can also run all by using the \"Runtime\" menu on the top menu bar. When you modify one of the reconstruction paramaters, think about what you expect the change to be.\n",
35 | "\n",
36 | "# Objectives\n",
37 | "* Reconstruct images from an actual scan.\n",
38 | "* Understand the core processing steps to reconstruct an image\n",
39 | "* Explore tradeoffs in reconstruction and sampling\n",
40 | "\n",
41 | "In python you need to load libraries to use them. This first cell imports a couple of key libraries to reconstruct images."
42 | ]
43 | },
44 | {
45 | "cell_type": "code",
46 | "metadata": {
47 | "id": "lXLlC5rOoIKI"
48 | },
49 | "source": [
50 | "# This is comment, Python will ignore this line\n",
51 | "\n",
52 | "# Import libraries (load libraries which provide some functions)\n",
53 | "%matplotlib inline\n",
54 | "import numpy as np # array library\n",
55 | "import math\n",
56 | "import cmath\n",
57 | "import pickle\n",
58 | "\n",
59 | "# For interactive plotting\n",
60 | "from ipywidgets import interact, interactive, FloatSlider, IntSlider\n",
61 | "from IPython.display import clear_output, display, HTML\n",
62 | "\n",
63 | "# for plotting modified style for better visualization\n",
64 | "import matplotlib.pyplot as plt\n",
65 | "import matplotlib as mpl\n",
66 | "mpl.rcParams['lines.linewidth'] = 3\n",
67 | "mpl.rcParams['axes.titlesize'] = 16\n",
68 | "mpl.rcParams['axes.labelsize'] = 11\n",
69 | "mpl.rcParams['xtick.labelsize'] = 10\n",
70 | "mpl.rcParams['ytick.labelsize'] = 10\n",
71 | "mpl.rcParams['legend.fontsize'] = 10"
72 | ],
73 | "execution_count": null,
74 | "outputs": []
75 | },
76 | {
77 | "cell_type": "markdown",
78 | "metadata": {
79 | "id": "9EOKnavHgwOV"
80 | },
81 | "source": [
82 | "# Download Raw Data\n",
83 | "We are going to download raw data from scans collected on the scanner. These are 2D brain scans collected with clinical sequence paramaters. There are 5 set of scans."
84 | ]
85 | },
86 | {
87 | "cell_type": "code",
88 | "metadata": {
89 | "id": "FcIOh7F5oK1-"
90 | },
91 | "source": [
92 | "# Get some data - Data is stored a Python pickled object\n",
93 | "import os\n",
94 | "if not os.path.exists(\"ReconExercise.tar\"):\n",
95 | " !wget https://www.dropbox.com/s/1w1izrohybpwqaw/ReconExercise.tar\n",
96 | " !tar xvf ReconExercise.tar\n",
97 | "\n",
98 | "# import the data\n",
99 | "scans = []\n",
100 | "for scan_number in range(5):\n",
101 | " filename = f'BrainScan{scan_number+1}.p'\n",
102 | "\n",
103 | " # Load the data\n",
104 | " dbfile = open(filename, 'rb')\n",
105 | " scan_data = pickle.load(dbfile)\n",
106 | " dbfile.close()\n",
107 | "\n",
108 | " # Fix a typo!\n",
109 | " if scan_data['Sequence'] == 'Spoiled Gradent Echo':\n",
110 | " scan_data['Sequence'] = 'Spoiled Gradient Echo'\n",
111 | "\n",
112 | " scans.append(scan_data)"
113 | ],
114 | "execution_count": null,
115 | "outputs": []
116 | },
117 | {
118 | "cell_type": "markdown",
119 | "metadata": {
120 | "id": "DJTpc32Xguqd"
121 | },
122 | "source": [
123 | "This loads the data. You can change the filename to by changing the series number."
124 | ]
125 | },
126 | {
127 | "cell_type": "markdown",
128 | "metadata": {
129 | "id": "Rnvw4a9_pesT"
130 | },
131 | "source": [
132 | "This just plots the raw k-space data. The vendor samples the readout direction at a rate 2x higher than that required to support the desired FOV. Some of the data sets are also padded at the edges of the k-space. Due to the way this was created, specifically a set step to convert a multi-coil experiment to a single coil step, those edges have some small values."
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "metadata": {
138 | "id": "1aAzhS_DwUoH"
139 | },
140 | "source": [
141 | "def plot_kspace(idx):\n",
142 | " scan_data = scans[idx]\n",
143 | "\n",
144 | " plt.figure(figsize=(8,4))\n",
145 | " plt.imshow(np.log(np.abs(scan_data['kspace'])),cmap='gray', aspect=0.5)\n",
146 | " plt.grid(False)\n",
147 | " shape = scan_data['kspace'].shape\n",
148 | " plt.title(f'Full Kspace Scan {scan_number} {shape}')\n",
149 | " plt.ylabel(r'$K_x$ [index]')\n",
150 | " plt.xlabel(r'$K_y$ [index]')\n",
151 | " plt.xticks([], [])\n",
152 | " plt.yticks([], [])\n",
153 | " plt.show()\n",
154 | "\n",
155 | "w = interactive(plot_kspace,\n",
156 | " idx=IntSlider(min=0, max=len(scans)-1, step=1, value=0, description='Scan number'))\n",
157 | "display(w)\n"
158 | ],
159 | "execution_count": null,
160 | "outputs": []
161 | },
162 | {
163 | "cell_type": "markdown",
164 | "metadata": {
165 | "id": "c572IIN_aGes"
166 | },
167 | "source": [
168 | "**Subsampling of the data**\n",
169 | "Below are some subsamplings of the data. They include:\n",
170 | "\n",
171 | "* Subselecting a set of lines by changing $\\Delta k_y $ controlled by accY\n",
172 | "* Subselecting a set of lines by changing $k_{max} $ in x and y controlled by kmaxX, kmaxY\n",
173 | "* Subselecting a set of point by randomly removing a set of points, controlled by random_undersampling_fraction (1=remove none)\n",
174 | "\n",
175 | "**Reconstruction**\n",
176 | "This is simple discrete Fourier transform of the data with a Gaussian window function.\n",
177 | "* What is the affect of changing the window function from 1.0 to 0.3 (suggest doing this with kmaxX,kmaxY = 1.0,1.0)\n",
178 | "\n",
179 | "\n",
180 | "**Suggested changes (all starting with other parameters = 1.0)**\n",
181 | "* Run with $\\Delta k_y$ = 1,2,3 see if you can predict what the image looks like from 1?\n",
182 | "* Run with kmaxY = 1.0, 0.5,0.1. What happens to resolution and noise? Do any artifacts come up?\n",
183 | "* Run with with random undersampling = 1.0 and 0.8. If the appearance less disrupting?\n",
184 | "\n",
185 | "**Questions**\n",
186 | "These are images in magnitude and phase. Some things to look at:\n",
187 | "* Is there any correspondance between magnitude and phase?\n",
188 | "* Is the phase uniform across the image?\n",
189 | "* What features apear bright and why?\n",
190 | "* Is there obervable Gibb's ringing?\n"
191 | ]
192 | },
193 | {
194 | "cell_type": "code",
195 | "metadata": {
196 | "id": "gL3QjhUxqVMS"
197 | },
198 | "source": [
199 | "def undersample_data(Kspace, accY, kmaxX, kmaxY, random_undersampling_fraction):\n",
200 | " # Subsampling\n",
201 | " Kspace_us = np.zeros_like(Kspace)\n",
202 | "\n",
203 | " # Regular - change delta k\n",
204 | " Kspace_us[:,::accY] = Kspace[:,::accY]\n",
205 | "\n",
206 | " # Sub sections - change kmax\n",
207 | " ky,kx = np.meshgrid( np.linspace(-1,1,Kspace.shape[1]),np.linspace(-1,1,Kspace.shape[0]))\n",
208 | " Kspace_us *= np.abs(kx/kmaxX) < 1\n",
209 | " Kspace_us *= np.abs(ky/kmaxY) < 1\n",
210 | "\n",
211 | " # Random undersampling\n",
212 | " mask = np.random.uniform(size=Kspace_us.shape[1:])\n",
213 | " mask = np.expand_dims(mask,axis=0)\n",
214 | " mask = np.repeat(mask,repeats=Kspace_us.shape[0],axis=0)\n",
215 | " Kspace_us *= mask < random_undersampling_fraction\n",
216 | "\n",
217 | " return Kspace_us\n",
218 | "\n",
219 | "def inverse_fourier_transform(Kspace_us, window):\n",
220 | " # Window function\n",
221 | " ky,kx = np.meshgrid( np.linspace(-1,1,Kspace_us.shape[1]),np.linspace(-1,1,Kspace_us.shape[0]))\n",
222 | " sigma = window\n",
223 | " kr = np.sqrt( kx**2 + ky**2)\n",
224 | " k_filter = np.exp( -(kr**2)/(2*sigma**2))\n",
225 | "\n",
226 | " # This is to do a FFT shift operator so that the center of k-space is at the center of the image\n",
227 | " x,y = np.meshgrid( range(Kspace_us.shape[1]), range(Kspace_us.shape[0]))\n",
228 | " chop = (-1)**( x+y)\n",
229 | "\n",
230 | " # Fourier Transform with Window Function\n",
231 | " Image = chop*np.fft.fft2(k_filter*Kspace_us*chop)\n",
232 | "\n",
233 | " return Image\n",
234 | "\n",
235 | "\n",
236 | "def sample_and_plot(scan_idx, accY, kmaxX, kmaxY, random_undersampling_fraction, window):\n",
237 | "\n",
238 | " # Grab the scan data\n",
239 | " scan_data = scans[scan_idx]\n",
240 | " Kspace = scan_data['kspace']\n",
241 | "\n",
242 | " # Subsample the data\n",
243 | " Kspace_us = undersample_data(Kspace, accY=accY, kmaxX=kmaxX, kmaxY=kmaxY, random_undersampling_fraction=random_undersampling_fraction)\n",
244 | "\n",
245 | " # Reconstruct that data\n",
246 | " image = inverse_fourier_transform( Kspace_us, window=window)\n",
247 | "\n",
248 | "\n",
249 | " crop = image.shape[0] // 4\n",
250 | "\n",
251 | " # Show the subsampled data\n",
252 | " plt.figure(figsize=(15,7))\n",
253 | "\n",
254 | " plt.subplot(131)\n",
255 | " plt.imshow(np.log(1e-7+np.abs(Kspace_us)),cmap='gray', aspect=0.5)\n",
256 | " plt.grid(False)\n",
257 | " shape = Kspace_us.shape\n",
258 | " plt.title(f'Subsampled Kspace Scan {scan_number} {shape}')\n",
259 | " plt.ylabel(r'$K_x$ [index]')\n",
260 | " plt.xlabel(r'$K_y$ [index]')\n",
261 | " plt.xticks([], [])\n",
262 | " plt.yticks([], [])\n",
263 | "\n",
264 | " plt.subplot(132)\n",
265 | " plt.imshow(np.abs(image[crop:-crop,:]),cmap='gray')\n",
266 | " plt.grid(False)\n",
267 | " plt.title(f'Reconstructed Image {scan_number}')\n",
268 | " plt.ylabel(r'$x$ [index]')\n",
269 | " plt.xlabel(r'$y$ [index]')\n",
270 | " plt.clim(0, 8*np.mean(np.abs(image)))\n",
271 | " plt.xticks([], [])\n",
272 | " plt.yticks([], [])\n",
273 | "\n",
274 | " plt.subplot(133)\n",
275 | " plt.imshow(np.angle(image[crop:-crop,:]),cmap='gray')\n",
276 | " plt.grid(False)\n",
277 | " plt.title(f'Reconstructed Phase {scan_number}')\n",
278 | " plt.ylabel(r'$x$ [index]')\n",
279 | " plt.xlabel(r'$y$ [index]')\n",
280 | " plt.xticks([], [])\n",
281 | " plt.yticks([], [])\n",
282 | "\n",
283 | " plt.show()\n",
284 | "\n",
285 | "\n",
286 | "w = interactive(sample_and_plot,\n",
287 | " scan_idx=IntSlider(min=0, max=len(scans)-1, step=1, value=0, description='Scan number', continuous_update=False),\n",
288 | " accY=IntSlider(min=1, max=4, step=1, value=1, description='Stride in Y', continuous_update=False),\n",
289 | " kmaxX=FloatSlider(min=0.1, max=1, step=0.1, value=1, description='Kmax X', continuous_update=False),\n",
290 | " kmaxY=FloatSlider(min=0.1, max=1, step=0.1, value=1, description='Kmax Y', continuous_update=False),\n",
291 | " window=FloatSlider(min=0.1, max=1.0, step=0.1, value=1.0, description='Window amount', continuous_update=False),\n",
292 | " random_undersampling_fraction=FloatSlider(min=0.1, max=1, step=0.1, value=1, description='Rand sample', continuous_update=False))\n",
293 | "\n",
294 | "display(w)\n"
295 | ],
296 | "execution_count": null,
297 | "outputs": []
298 | },
299 | {
300 | "cell_type": "markdown",
301 | "metadata": {
302 | "id": "Kxrcm0kLfQ91"
303 | },
304 | "source": [
305 | "Can you use these images to determine what scan this was? Check your answer by running the below code cell below.\n",
306 | "\n",
307 | "**There are five scans:**\n",
308 | "\n",
309 | "* Spin echo with Inversion Prep\n",
310 | " * TR = 9000ms\n",
311 | " * TE = 81ms\n",
312 | "\n",
313 | "* Spin Echo\n",
314 | " * TR = 6190ms\n",
315 | " * TE = 113ms\n",
316 | "\n",
317 | "* Spoiled Gradient Echo\n",
318 | " * TR = 250ms\n",
319 | " * TE = 3.4ms\n",
320 | " * Flip angle = 70\n",
321 | "\n",
322 | "* Spoiled Gradient Echo after Contrast\n",
323 | " * TR = 250ms\n",
324 | " * TE = 2.64ms\n",
325 | " * Flip angle = 70\n",
326 | "\n",
327 | "* Spin Echo\n",
328 | " * TR = 419ms\n",
329 | " * TE = 9.4ms\n",
330 | "\n",
331 | "\n",
332 | "**It may be useful to know some of the relaxation values in the brain:**\n",
333 | "* White Matter - The inner tissue of the brain\n",
334 | " * T1 = 1000 ms\n",
335 | " * T2 = 70 ms\n",
336 | "\n",
337 | "* Gray Matter - The thinner cortical tissue surroundings white matter. \n",
338 | " * T1 = 1800 ms\n",
339 | " * T2 = 100 ms\n",
340 | "\n",
341 | "* Cerebral Spinal Fluid - A water like fluid surroundings with two large cavities in the center of the brain.\n",
342 | " * T1 = 4000 ms\n",
343 | " * T2 = 800 ms\n",
344 | "\n",
345 | "\n"
346 | ]
347 | },
348 | {
349 | "cell_type": "code",
350 | "metadata": {
351 | "id": "ZZyYnkFJyu3z"
352 | },
353 | "source": [
354 | "for idx, scan_data in enumerate(scans):\n",
355 | " print(f'Scan number {idx}')\n",
356 | " print(' Sequence Type = ' + str(scan_data['Sequence']))\n",
357 | " print(' TR = ' + str(scan_data['TR']) + ' ms')\n",
358 | " print(' TE = ' + str(scan_data['TE']) + ' ms')\n",
359 | " if scan_data['Sequence'] == 'Spoiled Gradient Echo':\n",
360 | " print(' Flip = ' + str(scan_data['Flip']) + ' degrees')\n"
361 | ],
362 | "execution_count": null,
363 | "outputs": []
364 | }
365 | ]
366 | }
--------------------------------------------------------------------------------
/NoteBooks/Selective_RF_Excitation.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "name": "Selective_RF_Excitation.ipynb",
7 | "provenance": [],
8 | "authorship_tag": "ABX9TyNyHPGJteBHO5oAQnYarIxu",
9 | "include_colab_link": true
10 | },
11 | "kernelspec": {
12 | "name": "python3",
13 | "display_name": "Python 3"
14 | }
15 | },
16 | "cells": [
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {
20 | "id": "view-in-github",
21 | "colab_type": "text"
22 | },
23 | "source": [
24 | "
"
25 | ]
26 | },
27 | {
28 | "cell_type": "markdown",
29 | "metadata": {
30 | "id": "bPR9ILF2Sphi"
31 | },
32 | "source": [
33 | "# Spatially selective excitation\n",
34 | "This module will explore slice selection in which we aim to excite a slice (2D imaging) or slab (3D imaging).\n",
35 | "\n",
36 | "First we will need to run some code to import libraries and define a Bloch solver. This time the Bloch solver has some code to make it faster but slightly less accurate."
37 | ]
38 | },
39 | {
40 | "cell_type": "code",
41 | "metadata": {
42 | "id": "Qdw-4im7OpJa"
43 | },
44 | "source": [
45 | "# This is comment, Python will ignore this line\n",
46 | "\n",
47 | "# Import libraries (load libraries which provide some functions)\n",
48 | "%matplotlib inline\n",
49 | "import numpy as np # array library\n",
50 | "import math\n",
51 | "import cmath\n",
52 | "from scipy import interpolate\n",
53 | "import numba\n",
54 | "\n",
55 | "# For interactive plotting\n",
56 | "from ipywidgets import interact, interactive, FloatSlider, ToggleButton\n",
57 | "from IPython.display import clear_output, display, HTML\n",
58 | "\n",
59 | "# for plotting modified style for better visualization\n",
60 | "import matplotlib.pyplot as plt\n",
61 | "import matplotlib as mpl\n",
62 | "mpl.rcParams['lines.linewidth'] = 3\n",
63 | "mpl.rcParams['axes.titlesize'] = 14\n",
64 | "mpl.rcParams['axes.labelsize'] = 12\n",
65 | "mpl.rcParams['xtick.labelsize'] = 11\n",
66 | "mpl.rcParams['ytick.labelsize'] = 11\n",
67 | "mpl.rcParams['legend.fontsize'] = 11\n",
68 | "\n",
69 | "@numba.jit(nopython=True)\n",
70 | "def bloch_solver( B, time, freq=0, T1=2000, T2=2000, GAM=42.58e6*2*math.pi):\n",
71 | " # This is simple Rk4 solution to the Bloch Equations.\n",
72 | " #\n",
73 | " # Inputs:\n",
74 | " # B(array) -- Magentic Field [N x 3] (T)\n",
75 | " # time(array) -- Time of each point in waveforms (s)\n",
76 | " # freq -- Frequency [Hz]\n",
77 | " # T1 -- Longitudinal relaxation times (s)\n",
78 | " # T2 -- Transverse relaxation times (s)\n",
79 | " # M0 -- Initial state of magnetization (not equilibrium magnetization)\n",
80 | " # Outputs:\n",
81 | " # MOutput -- Magnetization for each position in time\n",
82 | "\n",
83 | " # Convert frequency to rads/s\n",
84 | " act_freq = 2*math.pi*freq\n",
85 | "\n",
86 | " #Convert to rotion rates (gamma*B)\n",
87 | " Bx = GAM*B[:,0]\n",
88 | " By = GAM*B[:,1]\n",
89 | " Bz = GAM*B[:,2] + act_freq\n",
90 | "\n",
91 | " # Double the resolution using linear interpolation (this is faster than splines)\n",
92 | " Bx2 = np.zeros( 2*len(Bz)+2)\n",
93 | " By2 = np.zeros( 2*len(Bz)+2)\n",
94 | " Bz2 = np.zeros( 2*len(Bz)+2)\n",
95 | " Bx2[:-4:2] = Bx[:-1]\n",
96 | " By2[:-4:2] = By[:-1]\n",
97 | " Bz2[:-4:2] = Bz[:-1]\n",
98 | "\n",
99 | " # Temp\n",
100 | " Bx2[1:-3:2] = 0.5*Bx[:-1] + 0.5*Bx[1:]\n",
101 | " By2[1:-3:2] = 0.5*By[:-1] + 0.5*By[1:]\n",
102 | " Bz2[1:-3:2] = 0.5*Bz[:-1] + 0.5*Bz[1:]\n",
103 | "\n",
104 | " #Initialize\n",
105 | " Mag = np.array([[0.0],[0.0],[1.0]])\n",
106 | "\n",
107 | " # Output storage\n",
108 | " MOutput = np.zeros_like(B)\n",
109 | " MOutput = np.expand_dims(MOutput,-1)\n",
110 | "\n",
111 | " #Runge-Kutta PDE Solution\n",
112 | " dt = time[2] - time[1]\n",
113 | " for count, t1 in enumerate(time):\n",
114 | "\n",
115 | " m1 = Mag\n",
116 | "\n",
117 | " bx = Bx2[count*2]\n",
118 | " by = By2[count*2]\n",
119 | " bz = Bz2[count*2]\n",
120 | " rhs = np.array([[ -1/T2, bz, -by],\n",
121 | " [ -bz ,-1/T2, bx],\n",
122 | " [ by , -bx, -1/T1]])\n",
123 | "\n",
124 | " k1 = np.dot(rhs, m1) + np.array([[0.0],[0.0],[1.0/T1]])\n",
125 | "\n",
126 | " t2 = t1 + dt/2\n",
127 | " bx = Bx2[count*2+1]\n",
128 | " by = By2[count*2+1]\n",
129 | " bz = Bz2[count*2+1]\n",
130 | " m2 = Mag + k1*dt/2\n",
131 | " k2 = np.dot(np.array([[ -1/T2, bz, -by],\n",
132 | " [ -bz ,-1/T2, bx],\n",
133 | " [ by , -bx, -1/T1]]), m2) + np.array([[0.0],[0.0],[1.0/T1]])\n",
134 | "\n",
135 | "\n",
136 | " t3 = t1 + dt/2\n",
137 | " bx = Bx2[count*2+1]\n",
138 | " by = By2[count*2+1]\n",
139 | " bz = Bz2[count*2+1]\n",
140 | " m3 = Mag + k2*dt/2\n",
141 | " k3 = np.dot(np.array([[ -1/T2, bz, -by],\n",
142 | " [ -bz ,-1/T2, bx],\n",
143 | " [ by , -bx, -1/T1]]), m3) + np.array([[0.0],[0.0],[1.0/T1]])\n",
144 | "\n",
145 | " t4 = t1 + dt\n",
146 | " bx = Bx2[count*2+2]\n",
147 | " by = By2[count*2+2]\n",
148 | " bz = Bz2[count*2+2]\n",
149 | " m4 = Mag + k3*dt\n",
150 | " k4 = np.dot(np.array([[ -1/T2, bz, -by],\n",
151 | " [ -bz ,-1/T2, bx],\n",
152 | " [ by , -bx, -1/T1]]), m4) + np.array([[0.0],[0.0],[1.0/T1]])\n",
153 | "\n",
154 | " # Runge-Kutta averages the above terms\n",
155 | " Mag = Mag + dt/6*(k1 + 2*k2 + 2*k3 + k4);\n",
156 | "\n",
157 | " # Save to an array\n",
158 | " MOutput[count,:]= Mag\n",
159 | "\n",
160 | " return MOutput"
161 | ],
162 | "execution_count": null,
163 | "outputs": []
164 | },
165 | {
166 | "cell_type": "markdown",
167 | "metadata": {
168 | "id": "Ad2hqIkE2PYP"
169 | },
170 | "source": [
171 | "# Sinc Pulses\n",
172 | "\n",
173 | "Not all pulses in MRI are Sinc pulses but we will consider pulses that are. Our pulses will have several paramaters:\n",
174 | "\n",
175 | "* **TBW** [unitless]: The time bandwidth product. This is effectively how many of the sinc lobes we include. More lobes means higher selectivity\n",
176 | "* **T** [s]: The time length of the RF pulse, this will control the total time the RF pulse takes. It will set *BW* [Hz], the bandwidth of the pulse in Hz\n",
177 | "* **Window function** : This is a function that rolls off the ends of the sinc so that there is a smoother transition when cutting off lobes. For this exercise I am using a hamming window but other options exist.\n",
178 | "\n",
179 | "The below code generated the pulse envelope. In this code, $B_1$ will be aligned in $x$ such that it rotates the magnetization into the $y$ direction. The sinc can also be modulated by a frequency to excite at a different center frequency.\n"
180 | ]
181 | },
182 | {
183 | "cell_type": "code",
184 | "metadata": {
185 | "id": "DH7oemUeix8C"
186 | },
187 | "source": [
188 | "def generate_sinc(T, TBW=4, window=True, dt=4e-6, GAM=42.58e6, flip=10, freq=0):\n",
189 | "\n",
190 | " # Number of points in waveform\n",
191 | " Nt = int(T/dt)\n",
192 | "\n",
193 | " # Time normalized to the time bandwidth product\n",
194 | " t = np.linspace(-TBW,TBW, Nt)\n",
195 | "\n",
196 | " # Get the pulse shape\n",
197 | " B1 = np.sinc(t)\n",
198 | "\n",
199 | " # To deal with the truncation we can apply a window function to taper the RF profile\n",
200 | " if window:\n",
201 | " B1 *= np.hamming(Nt)\n",
202 | "\n",
203 | " # Normalize to the flip angle\n",
204 | " B1 = B1 * (flip/360) / (GAM*np.sum(B1*dt))\n",
205 | "\n",
206 | " # Get actual time\n",
207 | " time = dt*np.arange(Nt)\n",
208 | "\n",
209 | " # Convert to complex with frequency\n",
210 | " B1 = B1*np.exp(2j*math.pi*time*freq)\n",
211 | "\n",
212 | " return time, B1\n",
213 | "\n",
214 | "def simulate_rf(time, B1):\n",
215 | " B = np.zeros( (len(B1),3))\n",
216 | " B[:,0] = np.real(B1)\n",
217 | " B[:,1] = np.imag(B1)\n",
218 | "\n",
219 | " Mout = bloch_solver( B, time, T1=2000, T2=2000, GAM=42.58e6*2*math.pi)\n",
220 | "\n",
221 | " return Mout\n",
222 | "\n",
223 | "def plot_rf(T, TBW, flip, freq, window):\n",
224 | " # Create Sinc\n",
225 | " time, B1 = generate_sinc(T/1e3, TBW, flip=flip, window=window, freq=freq)\n",
226 | "\n",
227 | " # Simulate\n",
228 | " Mout = simulate_rf( time, B1)\n",
229 | "\n",
230 | " plt.figure(figsize=(9,3))\n",
231 | " plt.subplot(121)\n",
232 | " plt.plot(1e3*time,np.real(B1),label='$B_x$')\n",
233 | " plt.plot(1e3*time,np.imag(B1),label='$B_y$')\n",
234 | " plt.xlabel('Time [ms]')\n",
235 | " plt.ylabel('$B_1$ [T]')\n",
236 | " plt.legend()\n",
237 | "\n",
238 | "\n",
239 | " plt.subplot(122)\n",
240 | " plt.plot(1e3*time,Mout[:,2], label=r'$M_z$')\n",
241 | " plt.plot(1e3*time,Mout[:,0], label=r'$M_x$')\n",
242 | " plt.plot(1e3*time,Mout[:,1], label=r'$M_y$')\n",
243 | " plt.xlabel('Time [ms]')\n",
244 | " plt.ylabel('Magnetization [a.u.]')\n",
245 | "\n",
246 | " plt.legend()\n",
247 | " plt.show()"
248 | ],
249 | "execution_count": null,
250 | "outputs": []
251 | },
252 | {
253 | "cell_type": "markdown",
254 | "metadata": {
255 | "id": "ehjOYmxi46Uj"
256 | },
257 | "source": [
258 | "# Sinc scaling parameters without gradients\n",
259 | "\n",
260 | "Below is a simulation using a standard Bloch simulator. The paramaters are set to maintain a constant flip angle for on-resonant spins. Try the following purtibations, first thinking what the affect might be on the peak $B_1$ which is often limited on systems.\n",
261 | "* Change the flip angle\n",
262 | "* Change the the TBW and T\n",
263 | "* Sweep the frequency, does the flip angle change? Is this different for a short and long pulse?\n"
264 | ]
265 | },
266 | {
267 | "cell_type": "code",
268 | "metadata": {
269 | "id": "dDrL89twPnvn"
270 | },
271 | "source": [
272 | "w = interactive(plot_rf,\n",
273 | " TBW=FloatSlider(min=1, max=12, step=1, value=2, description='TBW '),\n",
274 | " T=FloatSlider(min=0.5, max=10, step=0.5, value=2, description='T [ms]'),\n",
275 | " flip=FloatSlider(min=1, max=90, step=1, value=20, description='Flip [deg.]'),\n",
276 | " freq=FloatSlider(min=-2000, max=2000, step=100, value=0, description='RF Freq [Hz]'),\n",
277 | " window=ToggleButton(value=True,description='Toggle Window'))\n",
278 | "display(w)"
279 | ],
280 | "execution_count": null,
281 | "outputs": []
282 | },
283 | {
284 | "cell_type": "markdown",
285 | "metadata": {
286 | "id": "ImnWLOHmTyLx"
287 | },
288 | "source": [
289 | "# Adding a slice select gradient\n",
290 | " Now we will add a slice select gradient. In this code, the time of the pulse scales with the $TBW$ by:\n",
291 | "\\begin{equation}\n",
292 | "T = TBW*0.25 \\times 10^{-3}\n",
293 | "\\end{equation}\n",
294 | "\n",
295 | "This means that the bandwidth of the pulse ($BW$) is fixed to:\n",
296 | "\\begin{equation}\n",
297 | "BW=\\frac{TBW}{T} = \\frac{TBW}{TBW*0.25 \\times 10^{-3} [s]}= 4000 [Hz]\n",
298 | "\\end{equation}\n",
299 | "\n",
300 | "This makes seeing many of the effects much easier. Some questions to consider:\n",
301 | "\n",
302 | "* What is the effect of changing the center frequency? Does it depend on the gradient strength?\n",
303 | "* How does changing the gradient amplitude affect the slice thickness?\n",
304 | "* Does toggling the window function alter the slice profile?\n",
305 | "* What might be the practical need for the rephasing gradient?\n",
306 | "* Does the profile look the same for a 90 degree flip angle as a 15 degree? [the small tip angle aproximation will be violated and the responsse will not be a Fourier transform]\n"
307 | ]
308 | },
309 | {
310 | "cell_type": "code",
311 | "metadata": {
312 | "id": "aPvU8k0ekzHa"
313 | },
314 | "source": [
315 | "\n",
316 | "def simulate_rf_g(time, G, z, B1, T1, T2):\n",
317 | " B = np.zeros( (len(B1),3))\n",
318 | " B[:,0] = np.real(B1)\n",
319 | " B[:,1] = np.imag(B1)\n",
320 | " B[:,2] = G*z\n",
321 | "\n",
322 | " Mout = bloch_solver( B, time)\n",
323 | "\n",
324 | " return Mout\n",
325 | "\n",
326 | "def generate_sinc_and_grad( Gsel=1e-3, TBW=4, flip=10, window=True, freq=0, rephase=True):\n",
327 | "\n",
328 | " T = 0.25e-3*TBW\n",
329 | "\n",
330 | " # Generate RF\n",
331 | " time, B1 = generate_sinc( T, TBW, flip=flip, window=window, freq=freq)\n",
332 | "\n",
333 | " # Get delta time\n",
334 | " dt = time[1] - time[0]\n",
335 | "\n",
336 | " # Gradient for slice select (T/m)\n",
337 | " gselect = Gsel*np.ones_like(time)\n",
338 | "\n",
339 | " if rephase:\n",
340 | " # Generate rephaser\n",
341 | " Grephase = 20e-3 # amplitude of rephaser\n",
342 | " T_rephase = (Gsel * T * 0.5) / Grephase # Area / Gradient strength\n",
343 | " Nrephase = int(np.ceil( T_rephase / dt )) # number of points\n",
344 | " grephase = -Grephase*np.ones((Nrephase,)) #actual\n",
345 | " grephase = -grephase*0.5*np.sum(gselect)/np.sum(grephase)\n",
346 | "\n",
347 | " # Pad with zeros\n",
348 | " pad = np.zeros((20,))\n",
349 | " G = np.concatenate( (pad, gselect, grephase, pad))\n",
350 | " B1 = np.concatenate( (pad, B1, 0*grephase, pad))\n",
351 | " time = dt*np.arange(len(B1))\n",
352 | " else:\n",
353 | " # Pad with zeros\n",
354 | " pad = np.zeros((20,))\n",
355 | " G = np.concatenate( (pad, gselect, pad))\n",
356 | " B1 = np.concatenate( (pad, B1, pad))\n",
357 | " time = dt*np.arange(len(B1))\n",
358 | "\n",
359 | " return time, B1, G\n",
360 | "\n",
361 | "def plot_rf_g(TBW, flip, Gsel, freq, window, rephase):\n",
362 | "\n",
363 | " # Essentially ignore T1/T2\n",
364 | " T1=1000\n",
365 | " T2=1000\n",
366 | "\n",
367 | " # Convert to si units\n",
368 | " Gsel=Gsel/1e3 # mT/m to T/m\n",
369 | "\n",
370 | " # Create Sinc\n",
371 | " time, B1, G = generate_sinc_and_grad( Gsel=Gsel, TBW=TBW, flip=flip, window=window, freq=freq, rephase=rephase)\n",
372 | "\n",
373 | " ## Simulate\n",
374 | " zsim = np.linspace(-0.05,0.05,501) # Z values to simulate\n",
375 | " Mall = []\n",
376 | " for z in zsim:\n",
377 | " Mout = simulate_rf_g(time, G, z, B1, T1, T2)\n",
378 | " Mall.append(Mout)\n",
379 | " Mall = np.stack(Mall,axis=0)\n",
380 | "\n",
381 | " # Plots\n",
382 | " fig=plt.figure(figsize=(8,4))\n",
383 | "\n",
384 | " # Plot gradients\n",
385 | " plt.subplot(221)\n",
386 | " plt.plot(1e3*time, 1e3*G, color='b')\n",
387 | " plt.ylim([-25, 25])\n",
388 | " plt.xlabel('Time [ms]')\n",
389 | " plt.ylabel('G [mT/m]', color='b')\n",
390 | "\n",
391 | " # Plot B1\n",
392 | " plt.subplot(223)\n",
393 | " plt.plot(1e3*time,np.real(B1),label='$B_x$')\n",
394 | " plt.plot(1e3*time,np.imag(B1),label='$B_y$')\n",
395 | " plt.xlabel('Time [ms]')\n",
396 | " plt.ylabel('$B_1$ [T]')\n",
397 | " plt.legend()\n",
398 | " plt.ylim([-1.2*np.max(np.abs(B1)), 1.2*np.max(np.abs(B1))])\n",
399 | " plt.xlabel('Time [ms]')\n",
400 | "\n",
401 | " # Plot of Mz\n",
402 | " plt.subplot(222)\n",
403 | " plt.plot(zsim*1e3,Mall[:,-1,2],label='$M_z$')\n",
404 | " plt.xlabel('Position [mm]')\n",
405 | " plt.ylabel('$M_z$ [a.u.]')\n",
406 | "\n",
407 | " # Plot of Mxy\n",
408 | " plt.subplot(224)\n",
409 | " plt.plot(zsim*1e3,Mall[:,-1,1],label='$M_y$')\n",
410 | " plt.plot(zsim*1e3,Mall[:,-1,0],label='$M_x$')\n",
411 | " plt.xlabel('Position [mm]')\n",
412 | " plt.ylabel('M [a.u.]')\n",
413 | " plt.legend()\n",
414 | "\n",
415 | " plt.tight_layout(pad=0.4, w_pad=4.0, h_pad=1.0)\n",
416 | " plt.show()\n",
417 | "\n"
418 | ],
419 | "execution_count": null,
420 | "outputs": []
421 | },
422 | {
423 | "cell_type": "code",
424 | "metadata": {
425 | "id": "5LvoWJLpCjtO"
426 | },
427 | "source": [
428 | "\n",
429 | "w = interactive(plot_rf_g,\n",
430 | " TBW=FloatSlider(min=1, max=12, step=1, value=6, description='TBW',continuous_update=False),\n",
431 | " flip=FloatSlider(min=1, max=90, step=1, value=10, description='Flip [deg.]',continuous_update=False),\n",
432 | " freq=FloatSlider(min=-5000, max=5000, step=100, value=0, description='Freq [Hz]',continuous_update=False),\n",
433 | " Gsel=FloatSlider(min=3, max=20, step=1, value=10,description='Gsel [mT/m]', continuous_update=False),\n",
434 | " window=ToggleButton(value=True,description='Toggle Window',continuous_update=False),\n",
435 | " rephase=ToggleButton(value=True,description='Toggle Rephaser',continuous_update=False),\n",
436 | " )\n",
437 | "display(w)"
438 | ],
439 | "execution_count": null,
440 | "outputs": []
441 | }
442 | ]
443 | }
--------------------------------------------------------------------------------
/NoteBooks/Simulated_Sampling.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "name": "Simulated_Sampling.ipynb",
7 | "provenance": [],
8 | "include_colab_link": true
9 | },
10 | "kernelspec": {
11 | "name": "python3",
12 | "display_name": "Python 3"
13 | }
14 | },
15 | "cells": [
16 | {
17 | "cell_type": "markdown",
18 | "metadata": {
19 | "id": "view-in-github",
20 | "colab_type": "text"
21 | },
22 | "source": [
23 | "
"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {
29 | "id": "WxIjqKUIfTgl"
30 | },
31 | "source": [
32 | "# MRI Spatial Encoding\n",
33 | "\n",
34 | "This Jupyter notebook provides some hands on experience with designing a 2D sampling experiment.\n",
35 | "\n",
36 | "# Objectives\n",
37 | "* Understand the effect of changing readout parameters on images \n",
38 | "* Investigate tradeoffs between choices of readout parameters.\n",
39 | "\n",
40 | "In python you need to load libraries to use them. This first cell imports a couple of key libraries to reconstruct images."
41 | ]
42 | },
43 | {
44 | "cell_type": "code",
45 | "metadata": {
46 | "id": "lXLlC5rOoIKI"
47 | },
48 | "source": [
49 | "# This is comment, Python will ignore this line\n",
50 | "\n",
51 | "# Import libraries (load libraries which provide some functions)\n",
52 | "%matplotlib inline\n",
53 | "import numpy as np # array library\n",
54 | "import math\n",
55 | "import cmath\n",
56 | "import pickle\n",
57 | "import scipy.special\n",
58 | "\n",
59 | "# For interactive plotting\n",
60 | "from ipywidgets import interact, interactive, FloatSlider, IntSlider\n",
61 | "from IPython.display import clear_output, display, HTML\n",
62 | "\n",
63 | "# for plotting modified style for better visualization\n",
64 | "import matplotlib.pyplot as plt\n",
65 | "import matplotlib as mpl\n",
66 | "mpl.rcParams['lines.linewidth'] = 4\n",
67 | "mpl.rcParams['axes.titlesize'] = 24\n",
68 | "mpl.rcParams['axes.labelsize'] = 20\n",
69 | "mpl.rcParams['xtick.labelsize'] = 16\n",
70 | "mpl.rcParams['ytick.labelsize'] = 16\n",
71 | "mpl.rcParams['legend.fontsize'] = 16"
72 | ],
73 | "execution_count": null,
74 | "outputs": []
75 | },
76 | {
77 | "cell_type": "markdown",
78 | "metadata": {
79 | "id": "9EOKnavHgwOV"
80 | },
81 | "source": [
82 | "# Sampling Data\n",
83 | "We are going to makeup k-space data using an analytical phantom. The phantom consists of an exterior ring made of Fat with a chemical shift of 440 Hz (e.g. 3T main field), and an interior water compartment with some resolution objects. This cell defines the function and creates a ground truth image for reference."
84 | ]
85 | },
86 | {
87 | "cell_type": "code",
88 | "metadata": {
89 | "id": "FcIOh7F5oK1-"
90 | },
91 | "source": [
92 | "def k_shift(k, kx, ky,x_shift, y_shift):\n",
93 | " '''\n",
94 | " Shift a k-space signal by adding linear phase\n",
95 | " '''\n",
96 | " return k*np.exp(2j*math.pi*ky*y_shift)*np.exp(2j*math.pi*kx*x_shift)\n",
97 | "\n",
98 | "def k_square( kx, ky, x_shift, y_shift, w):\n",
99 | " '''\n",
100 | " Fourier transform of a square is sinc(kx)*sinc(ky)\n",
101 | " '''\n",
102 | " k =np.sinc(kx*w)*np.sinc(ky*w)*w*w\n",
103 | " return( k_shift(k, kx,ky,x_shift, y_shift))\n",
104 | "\n",
105 | "def k_circle( kx, ky, x_shift, y_shift, w):\n",
106 | " '''\n",
107 | " Fourier transform of a circle is bessel function\n",
108 | " '''\n",
109 | " kr = w*np.sqrt(kx**2 + ky**2)\n",
110 | " k = scipy.special.jv(1., kr * 2.0 * math.pi ) / (1e-6 + kr) * w**2\n",
111 | " return( k_shift(k, kx,ky,x_shift, y_shift))\n",
112 | "\n",
113 | "def bullseye_phantom( kx, ky, kt=None):\n",
114 | " '''\n",
115 | " Function to create a bullseye phantom\n",
116 | "\n",
117 | " Input:\n",
118 | " kx: k-space coordinates in kx [1/m]\n",
119 | " ky: k-space coordinates in ky [1/m]\n",
120 | " kt: time of each sample [s]\n",
121 | " Output:\n",
122 | " k_total: complex k-space at kx,ky,t\n",
123 | " '''\n",
124 | " # Central water\n",
125 | " k_water = np.zeros_like( kx, dtype=np.complex64)\n",
126 | " k_water += k_circle(kx, ky, 0.0, 0.0, 90e-3)\n",
127 | "\n",
128 | " # Some small squares\n",
129 | " widths = np.geomspace(1e-3,20e-3,10)\n",
130 | " shifts = 1.2*(np.cumsum(widths) - 0.6*np.sum(widths))\n",
131 | " for position_y, w in zip(shifts, widths):\n",
132 | " stride = 1.2*w\n",
133 | " for position_x in [-2,-1,0,1,2]:\n",
134 | " k_water += k_square(kx, ky, stride*position_x, position_y, w)\n",
135 | "\n",
136 | " # Outer fat\n",
137 | " k_fat = k_circle(kx, ky, 0.0, 0.0, 110e-3) - k_circle(kx, ky, 0.0, 0.0, 90e-3)\n",
138 | "\n",
139 | " if kt is not None:\n",
140 | " k_fat *= np.exp(2j*math.pi*440*kt)\n",
141 | "\n",
142 | " k_total = k_water + 2*k_fat\n",
143 | "\n",
144 | " return k_total\n",
145 | "\n",
146 | "\n",
147 | "# Defined size for phantom\n",
148 | "FOV = 0.240 # m\n",
149 | "dK = 1/FOV # m^-1\n",
150 | "N = 512\n",
151 | "kmax = N/2*dK\n",
152 | "\n",
153 | "# Evaluate k-space\n",
154 | "[kx, ky] = np.meshgrid( np.linspace(-kmax,kmax,N, dtype=np.float32), np.linspace(-kmax,kmax,N, dtype=np.float32))\n",
155 | "k = bullseye_phantom(kx,ky)\n",
156 | "\n",
157 | "# Reconstruct with FFT\n",
158 | "image = np.fft.fftshift(np.fft.ifft2(k))\n",
159 | "\n",
160 | "# Plot\n",
161 | "plt.figure(figsize=(20,20))\n",
162 | "plt.subplot(121)\n",
163 | "plt.imshow(np.log(np.abs(k)), cmap='gray')\n",
164 | "plt.xticks([], [])\n",
165 | "plt.yticks([], [])\n",
166 | "plt.title('Log transformed K-Space [truth]')\n",
167 | "plt.subplot(122)\n",
168 | "plt.imshow(np.abs(image), cmap='gray')\n",
169 | "plt.title('Image [truth]')\n",
170 | "plt.xticks([], [])\n",
171 | "plt.yticks([], [])\n",
172 | "plt.show()\n",
173 | "\n"
174 | ],
175 | "execution_count": null,
176 | "outputs": []
177 | },
178 | {
179 | "cell_type": "markdown",
180 | "metadata": {
181 | "id": "Rnvw4a9_pesT"
182 | },
183 | "source": [
184 | "# Creating a 2D sampling\n",
185 | "\n",
186 | "This sets up a sampling experiment very similar to how a scanner would. There are two system constraints in the code whcih we haven't discussed:\n",
187 | "* $G_{max}$ the maximum gradient strength. Typically 20-80 mT/m\n",
188 | "* $slew$ $rate$ the maximum rate the gradient can change, typically 80-200 T/m/s\n",
189 | "\n",
190 | "These are hardcoded below to $50 mT/m$ and $100 T/m/s$ but feel free to change them to see their influence. They are below:\n",
191 | "```\n",
192 | " gmax_system = 50e-3 # mT/m\n",
193 | " slew_system = 100 # T/m/s\n",
194 | "```\n",
195 | "\n",
196 | "## Readout Parameters\n",
197 | "There are a couple of ways to define sampling but we will define based on:\n",
198 | "* $BW [Hz]$ : the is effectively the strength of the frequency encoding gradient. We are using the definition $BW=\\gamma G_{freq} FOV$ where $FOV$ is the defined field-of-view in x. This $FOV$ is just a convention but will define the size of the reconstructed image in the frequency encoding direction.\n",
199 | "* $k_{max} [1/m]$ in x and y. The is the maximum area of the gradient.\n",
200 | "* $\\Delta k_y $ the spacing of phase encodes. This also sets the number of phase encodes $ N_{pe} = \\frac{2 k_{max} }{ \\Delta k_y } $\n",
201 | "\n",
202 | "## Experiments to try:\n",
203 | "* Change the $BW$, what happens to the sampling time and gradient strength? When might you use a high $BW$ vs. a low $BW$ ?\n",
204 | "* What parameters change the echo time?\n",
205 | "* Which parameters have the largest effect on scan time?\n",
206 | "* Why do we not have a $\\Delta k_x $?\n"
207 | ]
208 | },
209 | {
210 | "cell_type": "code",
211 | "metadata": {
212 | "id": "1aAzhS_DwUoH"
213 | },
214 | "source": [
215 | "def build_prephaser( area, slew, gmax, dT):\n",
216 | " \"\"\"\n",
217 | " Function to estimate gradient based on area.\n",
218 | "\n",
219 | " Input:\n",
220 | " area - area of gradient in s*T/m\n",
221 | " slew - slew rate of system in T/m/s\n",
222 | " gmax - maximum gradient strength of system\n",
223 | " dT - resolution of gradient system\n",
224 | "\n",
225 | " Output:\n",
226 | " g_trap - the gradient in T/m in the resolution of the system\n",
227 | " \"\"\"\n",
228 | "\n",
229 | " # This will help with some of the math\n",
230 | " t_rise = (gmax/slew)\n",
231 | " area_min_trap = t_rise * gmax\n",
232 | "\n",
233 | " if np.abs(area) < area_min_trap:\n",
234 | " # Triangle shaped gradient\n",
235 | " pwa = np.sqrt( np.abs(area) / slew )\n",
236 | " pw = 0\n",
237 | " else:\n",
238 | " # Trapezoid shaped gradient\n",
239 | " pwa = t_rise\n",
240 | " pw = (np.abs(area) - area_min_trap ) / gmax\n",
241 | "\n",
242 | " # Round to system resolution\n",
243 | " n_pw = int(np.round(pw/dT))\n",
244 | " n_pwa = int(np.round(pwa/dT))\n",
245 | "\n",
246 | " # Add the attack, flattop, decay\n",
247 | " g_trap = np.concatenate((np.linspace(0,1,n_pwa), np.ones((n_pw,)), np.linspace(1,0,n_pwa)))\n",
248 | " g_trap = g_trap * area / (dT*np.sum(g_trap))\n",
249 | "\n",
250 | " return g_trap\n",
251 | "\n",
252 | "def build_readout(BW, FOV, kmax, slew, gmax, dT, gamma=42.58e6):\n",
253 | " \"\"\"\n",
254 | " Function to estimate gradient for a readout\n",
255 | "\n",
256 | " Input:\n",
257 | " BW - Bandwidth of readout in Hz\n",
258 | " FOV - Field of view in m\n",
259 | " kmax - Maximum extent in k-space in 1/m\n",
260 | " slew - slew rate of system in T/m/s\n",
261 | " gmax - maximum gradient strength of system\n",
262 | " dT - resolution of gradient system\n",
263 | "\n",
264 | " Output:\n",
265 | " g_trap - the gradient in T/m in the resolution of the system\n",
266 | " pre_phase_area - the area required to phase the gradients\n",
267 | " idx - index array for readout\n",
268 | " \"\"\"\n",
269 | "\n",
270 | " # BW is in +/- Hz over the FOV (solve BW = gamma*G*FOV)\n",
271 | " amp_freq = BW / (gamma*FOV)\n",
272 | "\n",
273 | " # Get the central area where we sample\n",
274 | " area = 2.0*kmax / gamma\n",
275 | " Tread = area / amp_freq\n",
276 | " n_pw = int(np.round(Tread/dT))\n",
277 | "\n",
278 | " # Get Prephase\n",
279 | " Tramp = amp_freq / slew\n",
280 | " n_pwa = int(np.round(Tramp/dT))\n",
281 | "\n",
282 | " g_trap = np.concatenate((np.linspace(0,1,n_pwa), np.ones((n_pw,)), np.linspace(1,0,n_pwa)))\n",
283 | " g_trap *= amp_freq\n",
284 | "\n",
285 | " idx = np.concatenate((np.zeros(n_pwa,), np.ones((n_pw,)), np.zeros(n_pwa,)))\n",
286 | " idx = idx == 1\n",
287 | "\n",
288 | " pre_phase_area = -0.5*Tramp*amp_freq - 0.5*Tread*amp_freq\n",
289 | "\n",
290 | " return g_trap, pre_phase_area, idx\n",
291 | "\n",
292 | "def calc_2d_gradients( BW, kmax_x, kmax_y):\n",
293 | " \"\"\"\n",
294 | " Function to estimate gradient for a readout with phase encoding\n",
295 | "\n",
296 | " Input:\n",
297 | " BW - Bandwidth of readout in Hz\n",
298 | " kmax_x - Maximum extent in x [1/m]\n",
299 | " kmax_y - Maximum extent in y [1/m]\n",
300 | " Output:\n",
301 | " gx, gy - the gradient in mT/m in the resolution of the system\n",
302 | " \"\"\"\n",
303 | "\n",
304 | " # System paramaters (fixed in this simulation)\n",
305 | " FOV = 0.24 # m\n",
306 | " gmax_system = 50e-3 # mT/m\n",
307 | " slew_system = 100 # T/m/s\n",
308 | " gamma = 42.58e6\n",
309 | " dT_system = 4e-6\n",
310 | "\n",
311 | " # Build the gradients. This uses system limits\n",
312 | " gx_read, pre_phase_area, idx = build_readout(BW=BW,FOV=FOV, kmax=kmax_x, slew=slew_system, gmax=gmax_system, dT=dT_system)\n",
313 | " gx_pre = build_prephaser( pre_phase_area, slew=slew_system, gmax=gmax_system, dT=dT_system)\n",
314 | " gy_pre = build_prephaser( kmax_y / gamma, slew=slew_system, gmax=gmax_system, dT=dT_system)\n",
315 | "\n",
316 | " # Pad prephaser so that the pulses are the same length\n",
317 | " if len(gx_pre) > len(gy_pre):\n",
318 | " # Pad gy with zeros\n",
319 | " gy_pre = np.pad(gy_pre,(0,len(gx_pre)-len(gy_pre)))\n",
320 | " elif len(gx_pre) < len(gy_pre):\n",
321 | " # Pad x=gx with zeros\n",
322 | " gx_pre = np.pad(gx_pre,(len(gy_pre)-len(gx_pre),0))\n",
323 | "\n",
324 | " gx = np.concatenate((gx_pre,gx_read))\n",
325 | " gy = np.concatenate((gy_pre,0*gx_read))\n",
326 | "\n",
327 | " idx = np.concatenate((np.zeros((len(gx_pre,))), idx))\n",
328 | " idx = idx == 1.0\n",
329 | "\n",
330 | " # Convert to k-space\n",
331 | " kx = gamma*np.cumsum(gx)*dT_system\n",
332 | " ky = gamma*np.cumsum(gy)*dT_system\n",
333 | " kt = dT_system*np.arange(len(kx))\n",
334 | "\n",
335 | " return gx, gy, kx, ky, kt, idx\n",
336 | "\n",
337 | "def plot_gradient( BW, kmax_x, kmax_y, dk_y):\n",
338 | "\n",
339 | " # Calculate the gradients for the largest phase encode\n",
340 | " gx, gy, kx, ky, kt, idx = calc_2d_gradients( BW, kmax_x, kmax_y)\n",
341 | "\n",
342 | " # Scale gy for\n",
343 | " Npe = int( (2*kmax_y / dk_y) + 1)\n",
344 | " ky_scale = np.linspace(-1,1,Npe)\n",
345 | "\n",
346 | " plt.figure(figsize=(8,6))\n",
347 | "\n",
348 | " # Plot Gx\n",
349 | " plt.subplot(221)\n",
350 | " plt.plot(1e3*kt,1e3*gx)\n",
351 | " plt.ylabel('$G_x$ [mT/m]')\n",
352 | " plt.xlabel('$Time$ [us]')\n",
353 | " plt.ylim([-50, 50])\n",
354 | "\n",
355 | " # Plot Gy\n",
356 | " plt.subplot(223)\n",
357 | " for scale in ky_scale:\n",
358 | " plt.plot(1e3*kt,1e3*gy*scale)\n",
359 | " plt.ylabel('$G_y$ [mT/m]')\n",
360 | " plt.xlabel('$Time$ [ms]')\n",
361 | " plt.ylim([-50, 50])\n",
362 | "\n",
363 | " # Plot Kx\n",
364 | " plt.subplot(222)\n",
365 | " plt.plot(1e3*kt,kx/1e3)\n",
366 | " plt.ylabel('$K_x$ [1/mm]')\n",
367 | " plt.xlabel('$Time$ [ms]')\n",
368 | "\n",
369 | " # Plot Ky\n",
370 | " plt.subplot(224)\n",
371 | " for scale in ky_scale:\n",
372 | " plt.plot(1e3*kt,ky*scale/1e3)\n",
373 | " plt.ylabel('$K_y$ [1/mm]')\n",
374 | " plt.xlabel('$Time$ [ms]')\n",
375 | "\n",
376 | " plt.tight_layout(pad=2)\n",
377 | " plt.show()\n",
378 | "\n",
379 | "\n",
380 | "w = interactive(plot_gradient,\n",
381 | " BW=FloatSlider(min=62.5e3, max=250e3, value=125e3, description='BW [Hz]', continuous_update=False),\n",
382 | " kmax_x=FloatSlider(min=1/20e-3, max=1/1e-3, value=1/2e-3, step=1, description='Kmax x [1/m]', continuous_update=False),\n",
383 | " kmax_y=FloatSlider(min=1/20e-3, max=1/1e-3, value=1/2e-3, step=1, description='Kmax y [1/m]', continuous_update=False),\n",
384 | " dk_y=FloatSlider(min=1/240e-3, max=1/10e-3, value=1/10e-3, description='dky [1/m]', continuous_update=False))\n",
385 | "\n",
386 | "display(w)"
387 | ],
388 | "execution_count": null,
389 | "outputs": []
390 | },
391 | {
392 | "cell_type": "markdown",
393 | "metadata": {
394 | "id": "c572IIN_aGes"
395 | },
396 | "source": [
397 | "# Imaging Experiment\n",
398 | "\n",
399 | "This will simulate and plot images for the given phantom and parameters. You can alway reset the parameters by rerunning the cell. Some of the parameters will cause the cell to run for several seconds before it updates. Some experiments to try:\n",
400 | "\n",
401 | "1. Starting with the default parameters, change the BW higher and lower. What are the effects and which bandwidth would you choose based on this experiment?\n",
402 | "2. Repeat 1 but with higher noise levels. Does this change your answer to 1?\n",
403 | "3. Increase the $\\Delta k_y$. Do the artifacts look as you would expect?\n",
404 | "4. With no noise, change the $k_{max}$. Are there artifacts aside from the resolution differences?\n",
405 | "5. Repeat 4 with noise.\n",
406 | "6. With high noise levels, increase the number of averages. What is the effect?\n",
407 | "\n"
408 | ]
409 | },
410 | {
411 | "cell_type": "code",
412 | "metadata": {
413 | "id": "gL3QjhUxqVMS"
414 | },
415 | "source": [
416 | "def get_kspace_coordinates( BW, kmax_x, kmax_y, dk_y):\n",
417 | " \"\"\"\n",
418 | " Function to get k-space coordinates\n",
419 | "\n",
420 | " Input:\n",
421 | " BW: - Bandwidth of readout in Hz\n",
422 | " kmax_x: - Maximum extent in x [1/m]\n",
423 | " kmax_y: - Maximum extent in y [1/m]\n",
424 | " dk_y: - The spacing of ky phase encodes [1/m]\n",
425 | " Output:\n",
426 | " kx: - kx coordinates [1/m]\n",
427 | " ky: - kt coordinates [1/m]\n",
428 | " kt: - kt sampling time of each point in kspace\n",
429 | "\n",
430 | " \"\"\"\n",
431 | "\n",
432 | " # Calculate the gradients for the largest phase encode\n",
433 | " gx, gy, kx, ky, kt, idx = calc_2d_gradients( BW, kmax_x, kmax_y)\n",
434 | "\n",
435 | " # Scale gy for each phase encode\n",
436 | " Npe = int( (2*kmax_y / dk_y) + 1)\n",
437 | " ky_scale = np.linspace(-1,1,Npe)\n",
438 | "\n",
439 | " # gather kx,ky for all the phase encodes\n",
440 | " kx_2 = []\n",
441 | " ky_2 = []\n",
442 | " kt_2 = []\n",
443 | " for scale in ky_scale:\n",
444 | " kx_2.append(kx[idx])\n",
445 | " ky_2.append(scale*ky[idx])\n",
446 | " kt_2.append(kt[idx])\n",
447 | " kx = np.stack(kx_2,axis=0)\n",
448 | " ky = np.stack(ky_2,axis=0)\n",
449 | " kt = np.stack(kt_2,axis=0)\n",
450 | "\n",
451 | " return kx, ky, kt\n",
452 | "\n",
453 | "def sim_and_plot( BW=250e3, kmax_x=1/10e-3, kmax_y=1/10e-3, dk_y=1/240e-3, noise=0, averages=1):\n",
454 | "\n",
455 | " # Get y\n",
456 | " kx, ky, kt = get_kspace_coordinates( BW=BW, kmax_x=kmax_x, kmax_y=kmax_y, dk_y=dk_y)\n",
457 | "\n",
458 | " # Get kspace values\n",
459 | " k0 = bullseye_phantom(0,0)\n",
460 | "\n",
461 | " # Add noise\n",
462 | " kspace = averages*bullseye_phantom(kx, ky, kt)\n",
463 | " for a in range(averages):\n",
464 | " kspace += k0*noise*(np.random.standard_normal(kspace.shape) + 1j*np.random.standard_normal(kspace.shape))\n",
465 | "\n",
466 | " # Reconstruct with Fourier transform\n",
467 | " image = np.fft.fftshift(np.fft.ifft2(kspace))\n",
468 | "\n",
469 | " # For plotting the image size will change based on BW. Adjust to suite\n",
470 | " BW_system = 1 / (kt[0,1] - kt[0,0])\n",
471 | " aspect = BW_system / BW\n",
472 | " crop = np.abs(np.linspace(-aspect, aspect, image.shape[1])) < 1.0\n",
473 | "\n",
474 | " # Also adjust y fov to match reconstructed size\n",
475 | " aspect_y = 1/240e-3 / dk_y\n",
476 | "\n",
477 | " print(f'Minimum scan time = {np.max(kt)*ky.shape[0]*averages}, Echo time = {1e3*np.mean(kt)} ms')\n",
478 | "\n",
479 | " plt.figure(figsize=(8,8))\n",
480 | " plt.imshow(np.abs(image[:,crop]),aspect=aspect_y,cmap='gray')\n",
481 | " plt.xticks([], [])\n",
482 | " plt.yticks([], [])\n",
483 | " plt.show()\n",
484 | "\n",
485 | "w = interactive(sim_and_plot,\n",
486 | " BW=FloatSlider(min=62.5e3, max=250e3, value=125e3, description='BW [Hz]', continuous_update=False),\n",
487 | " kmax_x=FloatSlider(min=1/20e-3, max=1/1e-3, value=1/2e-3, step=1, description='Kmax x [1/m]', continuous_update=False),\n",
488 | " kmax_y=FloatSlider(min=1/20e-3, max=1/1e-3, value=1/2e-3, step=1, description='Kmax y [1/m]', continuous_update=False),\n",
489 | " dk_y=FloatSlider(min=1/240e-3, max=1/80e-3, value=1/480e-3, step=1/480e-3, description='dky [1/m]', continuous_update=False),\n",
490 | " noise=FloatSlider(min=0, max=0.1, value=0, step=1e-6, description='noise level', continuous_update=False),\n",
491 | " averages=IntSlider(min=1, max=4, value=1, description='averages', continuous_update=False))\n",
492 | "\n",
493 | "display(w)"
494 | ],
495 | "execution_count": null,
496 | "outputs": []
497 | }
498 | ]
499 | }
--------------------------------------------------------------------------------
/NoteBooks/Spin_Echo.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "name": "Spin_Echo.ipynb",
7 | "provenance": [],
8 | "authorship_tag": "ABX9TyPRBsjwSIV5vEsXQM2yxiAi",
9 | "include_colab_link": true
10 | },
11 | "kernelspec": {
12 | "name": "python3",
13 | "display_name": "Python 3"
14 | }
15 | },
16 | "cells": [
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {
20 | "id": "view-in-github",
21 | "colab_type": "text"
22 | },
23 | "source": [
24 | "
"
25 | ]
26 | },
27 | {
28 | "cell_type": "markdown",
29 | "metadata": {
30 | "id": "dqr2kH73Xfyf"
31 | },
32 | "source": [
33 | "# Spin Echo\n",
34 | "In this notebook, we will explore the solutions to the Bloch equations for a spin gradient echo sequence. Recall the spin echo has a timing diagram that looks like:\n",
35 | "\n",
36 | "\n",
37 | "\n",
38 | "where $TR$ is the time between $90^{\\circ}$ excitations and TE is the double the spacing between excitations.\n"
39 | ]
40 | },
41 | {
42 | "cell_type": "code",
43 | "metadata": {
44 | "id": "_kXRe1UtXPs6"
45 | },
46 | "source": [
47 | "# This is comment, Python will ignore this line\n",
48 | "\n",
49 | "# Import libraries (load libraries which provide some functions)\n",
50 | "%matplotlib inline\n",
51 | "import numpy as np # array library\n",
52 | "import math\n",
53 | "import cmath\n",
54 | "\n",
55 | "# For interactive plotting\n",
56 | "from ipywidgets import interact, interactive, FloatSlider\n",
57 | "from IPython.display import clear_output, display, HTML\n",
58 | "\n",
59 | "# for plotting modified style for better visualization\n",
60 | "import matplotlib.pyplot as plt\n",
61 | "import matplotlib as mpl\n",
62 | "mpl.rcParams['lines.linewidth'] = 2\n",
63 | "mpl.rcParams['axes.titlesize'] = 18\n",
64 | "mpl.rcParams['axes.labelsize'] = 14\n",
65 | "mpl.rcParams['xtick.labelsize'] = 12\n",
66 | "mpl.rcParams['ytick.labelsize'] = 12\n",
67 | "mpl.rcParams['legend.fontsize'] = 12\n",
68 | "\n",
69 | "# Hit the play button to run this cell"
70 | ],
71 | "execution_count": null,
72 | "outputs": []
73 | },
74 | {
75 | "cell_type": "markdown",
76 | "metadata": {
77 | "id": "p7_bvi4tWCr7"
78 | },
79 | "source": [
80 | "# Import a solver\n",
81 | "\n",
82 | "This will import the nutation solver we had before."
83 | ]
84 | },
85 | {
86 | "cell_type": "code",
87 | "metadata": {
88 | "id": "VwOSaC97r5IW"
89 | },
90 | "source": [
91 | "class Event:\n",
92 | " def __init__(self, excite_flip=0, excite_phase=0, recovery_time=0, spoil=False):\n",
93 | " self.excite_flip = excite_flip\n",
94 | " self.excite_phase = excite_phase\n",
95 | " self.recovery_time = recovery_time\n",
96 | " self.spoil = spoil\n",
97 | "\n",
98 | "def bloch_nutation_solver( event_list, M0, T1, T2, freq ):\n",
99 | "\n",
100 | " # Inputs:\n",
101 | " # event_list -- Special structure with entries\n",
102 | " # .excite_flip flip angle of rotation\n",
103 | " # .excite_phase phase of excite degrees\n",
104 | " # .recovery_time time after excite to recover\n",
105 | " # .spoil (if 'true' this set the Mxy to zero at the recovery)\n",
106 | " # T1 -- Longitudinal relaxation times (s)\n",
107 | " # T2 -- Transverse relaxation times (s)\n",
108 | " # Freq Offset-- Off center frequency in Hz\n",
109 | " # M0 -- Initial state of magnetization (not equilibrium magnetization)\n",
110 | " # Outputs:\n",
111 | " # time -- Magnetization for each position in time\n",
112 | " # BOutput -- Magnetic field for each position in time (interpolated)\n",
113 | "\n",
114 | " # Initialize\n",
115 | " time = [0,]\n",
116 | " Mout = [M0,]\n",
117 | "\n",
118 | " M=M0;\n",
119 | "\n",
120 | " # Go through the event_list\n",
121 | " for event in event_list:\n",
122 | "\n",
123 | " theta = event.excite_phase * math.pi / 180\n",
124 | " alpha = event.excite_flip * math.pi / 180\n",
125 | " T = event.recovery_time\n",
126 | " spoil = event.spoil\n",
127 | "\n",
128 | " # Excite\n",
129 | " Rz = np.array([[math.cos(theta), math.sin(theta), 0],\n",
130 | " [-math.sin(theta), math.cos(theta), 0],\n",
131 | " [0, 0, 1]])\n",
132 | " Rx = np.array([[1,0, 0],\n",
133 | " [0, math.cos(alpha), math.sin(alpha)],\n",
134 | " [0, -math.sin(alpha), math.cos(alpha)]])\n",
135 | "\n",
136 | " M = np.linalg.inv(Rz)@Rx@Rz@M\n",
137 | "\n",
138 | " # Relaxation (Transverse)\n",
139 | " if spoil:\n",
140 | " Mxy = np.array(0 + 1j*0)\n",
141 | " Mx = 0\n",
142 | " My = 0\n",
143 | " else:\n",
144 | " Mxy = M[0] + 1j*M[1]\n",
145 | " Mxy = Mxy*cmath.exp( 2j*math.pi*freq*T)*math.exp(-T/T2)\n",
146 | " Mx = Mxy[0].real\n",
147 | " My = Mxy[0].imag\n",
148 | "\n",
149 | "\n",
150 | " # Relaxation (Longitudinal)\n",
151 | " Mz = M[2]\n",
152 | " Mz = 1 + (Mz - 1)*math.exp(-T/T1);\n",
153 | "\n",
154 | " # Put back into [Mx; My; Mz] vector\n",
155 | " M = np.array([[Mx], [My], [Mz[0]]])\n",
156 | "\n",
157 | " # Store for output\n",
158 | " time.append(time[-1]+T)\n",
159 | " Mout.append(M)\n",
160 | "\n",
161 | " Mout = np.array(Mout)\n",
162 | " return time, Mout\n",
163 | ""
164 | ],
165 | "execution_count": null,
166 | "outputs": []
167 | },
168 | {
169 | "cell_type": "markdown",
170 | "metadata": {
171 | "id": "ZagoXPzmabT0"
172 | },
173 | "source": [
174 | "# Solution for Spin Echo\n",
175 | "Now lets solve for a spin echo experiment. The key paramaters we have are\n",
176 | "\n",
177 | "* $TR$ sets the time between pulses, typically from 0.5s to 10s\n",
178 | "* $TE$ would be the time after the excitation to the spin echo and would detrmine how much $T2$ weighting in the image. Typically this is 20-200ms.\n",
179 | "\n"
180 | ]
181 | },
182 | {
183 | "cell_type": "code",
184 | "metadata": {
185 | "id": "aCkkIEBAzrR0"
186 | },
187 | "source": [
188 | "# Define this as a function to make this reusable\n",
189 | "def make_spin_echo(TR, TE):\n",
190 | " # Blank list of events\n",
191 | " event_list = []\n",
192 | "\n",
193 | " for excite_number in range(2):\n",
194 | " # 90x - Excite\n",
195 | " event_list.append( Event(excite_flip=90, excite_phase=0))\n",
196 | "\n",
197 | " # Gap (split into multiple gaps just to show recovery)\n",
198 | " dt = (TE/2) / 100\n",
199 | " for pos in range(100):\n",
200 | " event_list.append( Event(recovery_time=dt))\n",
201 | "\n",
202 | " # 180y - Refocusing Pulse\n",
203 | " event_list.append( Event(excite_flip=180, excite_phase=90))\n",
204 | "\n",
205 | " # Time to the end of the pulse TR\n",
206 | " dt = (TR-TE/2) / 500\n",
207 | " for pos in range(500):\n",
208 | " event_list.append( Event(recovery_time=dt))\n",
209 | "\n",
210 | " return event_list\n",
211 | ""
212 | ],
213 | "execution_count": null,
214 | "outputs": []
215 | },
216 | {
217 | "cell_type": "markdown",
218 | "metadata": {
219 | "id": "bMZ_swNVg6eO"
220 | },
221 | "source": [
222 | "# Effect of parameters on the signal evolution (Single Frequency)\n",
223 | "Below are plots of the signal evolution over time for a single frequency for two 90-180 pulses. You can play around with paramaters to see if they change as you expect. Pay attention to the magnetization at the end of the chain. Do you get a spin echo for this case?\n"
224 | ]
225 | },
226 | {
227 | "cell_type": "code",
228 | "metadata": {
229 | "id": "Bzh3yKS8dN2C"
230 | },
231 | "source": [
232 | "def spin_echo_sim(T1, T2, freq_offset, TR, TE):\n",
233 | "\n",
234 | " # Simulate\n",
235 | " M0 = np.array([[0],[0],[1.0]])\n",
236 | " event_list = make_spin_echo(TR, TE)\n",
237 | " time, Mout = bloch_nutation_solver( event_list, M0, T1, T2, freq_offset );\n",
238 | "\n",
239 | " plt.figure(figsize=(8,4))\n",
240 | " plt.plot(time,Mout[:,0], label=r'$M_x$')\n",
241 | " plt.plot(time,Mout[:,1], label=r'$M_y$')\n",
242 | " plt.plot(time,np.sqrt(Mout[:,1]**2+Mout[:,0]**2), '--', label=r'$M_y$')\n",
243 | " plt.plot(time,Mout[:,2], label=r'$M_z$')\n",
244 | " plt.ylabel('Magnetization [a.u.]')\n",
245 | " plt.xlabel('Time [s]')\n",
246 | " plt.legend()\n",
247 | " plt.show()\n",
248 | "\n",
249 | "w = interactive(spin_echo_sim,\n",
250 | " T1=FloatSlider(min=0.3, max=5, step=0.1, value=1,description='T1 [s]'),\n",
251 | " T2=FloatSlider(min=0.1, max=0.5, step=0.01, value=0.4, description='T2 [s]'),\n",
252 | " TE=FloatSlider(min=10e-3, max=300e-3, step=1e-3, value=300e-3, description='TE [s]'),\n",
253 | " TR=FloatSlider(min=500e-3, max=5, step=0.1, value=2, description='TR [s]'),\n",
254 | " freq_offset=FloatSlider(min=-20, max=20, step=0.1, value=2, description='Freq. off. [Hz]'))\n",
255 | "display(w)"
256 | ],
257 | "execution_count": null,
258 | "outputs": []
259 | },
260 | {
261 | "cell_type": "markdown",
262 | "metadata": {
263 | "id": "6NUDesem0Geb"
264 | },
265 | "source": [
266 | "# Effect of parameters on the signal evolution (Multiple Frequency)\n",
267 | "Below are plots of the signal evolution over time for multiple frequencies (simulating the R2' effect). You can play around with paramaters to see if they change as you expect. Pay attention to the magnetization at the end of the chain. Do you get a spin echo for this case?\n",
268 | "\n",
269 | "* The red plots are the $M_x$ signal for a some of the frequencies simulated\n",
270 | "* The blue plots are the $M_y$ signal for a some of the frequencies simulated\n",
271 | "* The black plot is the sum of the signal across a range of frequencies\n",
272 | "* In this plot, I am only showing the second excite which is more representative.\n",
273 | "\n",
274 | "**Warning**: The update of this simulation is a bit slow due to the need to simulate multiple frequencies.\n"
275 | ]
276 | },
277 | {
278 | "cell_type": "code",
279 | "metadata": {
280 | "id": "VNDitXvalA1V"
281 | },
282 | "source": [
283 | "def spin_echo_sim_mf(T1, T2, deltaB0, TR, TE):\n",
284 | "\n",
285 | " Nsim = 101\n",
286 | " B0range = np.linspace(-deltaB0/2,deltaB0,Nsim)\n",
287 | "\n",
288 | " # Simulate\n",
289 | " Mall = []\n",
290 | " plt.figure(figsize=(8,4))\n",
291 | " for count, freq_offset in enumerate(B0range):\n",
292 | " M0 = np.array([[0],[0],[1.0]])\n",
293 | " event_list = make_spin_echo(TR, TE)\n",
294 | " time, Mout = bloch_nutation_solver( event_list, M0, T1, T2, freq_offset );\n",
295 | "\n",
296 | " if count %30==0:\n",
297 | " if count ==0:\n",
298 | " plt.plot(time,Mout[:,0], color='lightsteelblue',label=r'$M_x$')\n",
299 | " plt.plot(time,Mout[:,1], color='lightpink', label=r'$M_y$')\n",
300 | " else:\n",
301 | " plt.plot(time,Mout[:,0], color='lightsteelblue',label=None)\n",
302 | " plt.plot(time,Mout[:,1], color='lightpink', label=None)\n",
303 | "\n",
304 | " Mall.append(Mout)\n",
305 | "\n",
306 | " Mout = 0*Mall[0]\n",
307 | " for M in Mall:\n",
308 | " Mout += M / Nsim\n",
309 | " plt.plot(time,np.sqrt(Mout[:,1]**2+Mout[:,0]**2), color='k', label=r'$|M_{xy}|$')\n",
310 | " plt.ylabel('Magnetization [a.u.]')\n",
311 | " plt.xlabel('Time [s]')\n",
312 | " plt.xlim([np.amax(time)-TR, np.amax(time)])\n",
313 | " plt.legend()\n",
314 | " plt.show()\n",
315 | "\n",
316 | "w = interactive(spin_echo_sim_mf,\n",
317 | " T1=FloatSlider(min=0.3, max=5, step=0.1, value=1,description='T1 [s]'),\n",
318 | " T2=FloatSlider(min=0.1, max=0.5, step=0.01, value=0.4, description='T2 [s]'),\n",
319 | " TE=FloatSlider(min=10e-3, max=300e-3, step=1e-3, value=300e-3, description='TE[s]'),\n",
320 | " TR=FloatSlider(min=500e-3, max=2, step=0.1, value=2, description='TR[s]'),\n",
321 | " deltaB0=FloatSlider(min=0, max=20, step=1, value=5, description='B0 range [Hz]'))\n",
322 | "display(w)\n"
323 | ],
324 | "execution_count": null,
325 | "outputs": []
326 | }
327 | ]
328 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Intro-to-MRI
2 | This is Jupyter notebook/python code developed for a UW-Madison introductory MRI class. The notebooks were made to support Google colab markdown but they should also open up in a standard jupyter enviroment. Most notebooks have a link on top of the page to allow you to open it directly from github.
3 |
4 | # Notebooks
5 | 1. [Intro to Bloch Solvers](NoteBooks/Intro_MRI_Bloch_Solvers.ipynb) : This notebooks introduces two ways to simulate the Bloch equations using either standard solvers or solvers assuming periods of free relaxation.
6 | 2. [Spoiled Gradient Echo](NoteBooks/Spoiled_Gradient_Echo.ipynb) : This notebook simulates spoiled gradient echo as a means to create contrast in images.
7 | 3. [Spin Echo](NoteBooks/Spin_Echo.ipynb) : This notebook simulates spin echo as a means to create contrast in images.
8 | 4. [Basic Images](NoteBooks/BasicWeightedImages.ipynb) : The notebook simulated basic spin and gradient echo images for a digital brain phantom.
9 | 5. [Spatial Selective RF](NoteBooks/Selective_RF_Excitation.ipynb) : This uses sinc pulses to investigate the tradeoffs in RF pulse choices.
10 | 6. [Cartesian Sampling](NoteBooks/Simulated_Sampling.ipynb) : This uses fake data to examine undersampling and reconstruction.
11 | 7. [Cartesian Sampling Real Data](NoteBooks/Recon_Example.ipynb) : This uses real data to examine undersampling and reconstruction.
12 | 8. [Magnetic Field Generation](NoteBooks/Field_Generation.ipynb) : Code to create magnetic fields from loops of wire.
13 |
14 | # Advanced Notebooks
15 | 1. [Variation Networks](AdvancedNoteBooks/VarNetToyExample.ipynb) : Toy example of using model based machine learning reconstruction to reconstruction images with reduced artifacts.
16 | 2. [Compressed Sensing](AdvancedNoteBooks/Constrained_Reconstruction_Demo.ipynb) : Example using parallel imaging and compressed SENSING
17 |
18 | # Simulations
19 | 1. [EPI Distortions](Simulations/epi_distortions.py) : Python code to simulate EPI distortions using brute force forward model with off-resonance.
20 | 2. [Spiral Distortions](Simulations/spiral_distortions.py) : Python code to simulate spiral distortions using brute force forward model with off-resonance.
21 | 3. [Complex Demodulation](Simulations/demodulation_example.py) : Python code which shows the basic steps to convert real valued detected signal to complex signal in the rotating frame.
22 |
23 |
--------------------------------------------------------------------------------
/Simulations/convert_data.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 21,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import numpy as np\n",
10 | "import matplotlib.pyplot as plt\n",
11 | "import os\n",
12 | "import glob\n",
13 | "import h5py"
14 | ]
15 | },
16 | {
17 | "cell_type": "code",
18 | "execution_count": 22,
19 | "metadata": {},
20 | "outputs": [
21 | {
22 | "name": "stdout",
23 | "output_type": "stream",
24 | "text": [
25 | "subject54_muscles_v T1 = 900 T2 = 50 T2star = 40 density = 1 susceptibility = -9.05\n",
26 | "subject54_bck_v T1 = 800 T2 = 800 T2star = 800 density = 0.0 susceptibility = 0.36\n",
27 | "subject54_marrow_v T1 = 1000 T2 = 50 T2star = 40 density = 1.0 susceptibility = -9.05\n",
28 | "subject54_muscles_skin_v T1 = 2569 T2 = 329 T2star = 58 density = 1.0 susceptibility = -9.05\n",
29 | "subject54_wm_v T1 = 500 T2 = 70 T2star = 61 density = 0.77 susceptibility = -9.05\n",
30 | "subject54_fat_v T1 = 250 T2 = 70 T2star = 60 density = 1.0 susceptibility = -7.79\n",
31 | "subject54_fat2_v T1 = 250 T2 = 70 T2star = 60 density = 1.0 susceptibility = -7.79\n",
32 | "subject54_vessels T1 = 1600 T2 = 80 T2star = 70 density = 1.0 susceptibility = -9\n",
33 | "subject54_dura_v T1 = 1000 T2 = 100 T2star = 50 density = 0.6 susceptibility = -9.05\n",
34 | "subject54_gm_v T1 = 900 T2 = 90 T2star = 69 density = 0.86 susceptibility = -9.05\n",
35 | "subject54_csf_v T1 = 4000 T2 = 2000 T2star = 2000 density = 1.0 susceptibility = -9.05\n",
36 | "subject54_skull_v T1 = 300 T2 = 1 T2star = 1 density = 0.1 susceptibility = -8.86\n"
37 | ]
38 | }
39 | ],
40 | "source": [
41 | "# Add Brainweb files have same extension\n",
42 | "files = glob.glob('*.rawb')\n",
43 | "\n",
44 | "class Tissue:\n",
45 | " '''\n",
46 | " Tissue class to store MRI data and parameters\n",
47 | " '''\n",
48 | "\n",
49 | " def __init__(self, volume):\n",
50 | " self.volume = volume\n",
51 | " self.T1 = -1\n",
52 | " self.T2 = -1\n",
53 | " self.T2star = -1\n",
54 | " self.density = -1\n",
55 | " self.susceptibility = -1\n",
56 | " self.name = 'None'\n",
57 | " self.freq = 0.0 \n",
58 | "\n",
59 | " def __getitem__(self, idx):\n",
60 | " return ((self.volume[idx] + 128) / 255)\n",
61 | "\n",
62 | " def __setitem__(self, idx, value):\n",
63 | " self.volume[idx] = value\n",
64 | "\n",
65 | " def export(self, filename):\n",
66 | " \n",
67 | " with h5py.File(filename, 'w') as f:\n",
68 | " f.create_dataset('volume', data=self.volume, compression='gzip')\n",
69 | " f.attrs['T1'] = self.T1\n",
70 | " f.attrs['T2'] = self.T2\n",
71 | " f.attrs['T2star'] = self.T2star\n",
72 | " f.attrs['density'] = self.density\n",
73 | " f.attrs['susceptibility'] = self.susceptibility\n",
74 | " f.attrs['name'] = self.name\n",
75 | " f.attrs['freq'] = self.freq\n",
76 | "\n",
77 | " def load(self, filename):\n",
78 | " \n",
79 | " with h5py.File(filename, 'r') as f:\n",
80 | " self.volume = np.array(f['volume'])\n",
81 | " self.T1 = f.attrs['T1']\n",
82 | " self.T2 = f.attrs['T2']\n",
83 | " self.T2star = f.attrs['T2star']\n",
84 | " self.density = f.attrs['density']\n",
85 | " self.susceptibility = f.attrs['susceptibility']\n",
86 | " self.name = f.attrs['name']\n",
87 | " self.freq = f.attrs['freq']\n",
88 | "\n",
89 | " \n",
90 | "# Load the data and create the Tissue objects\n",
91 | "tissues = []\n",
92 | "for file in files:\n",
93 | " data = np.fromfile(file, dtype=np.int8)\n",
94 | " data = data.reshape((362, 434, 362))\n",
95 | "\n",
96 | " file_prefix = os.path.splitext(file)[0]\n",
97 | "\n",
98 | " tissue = Tissue(data)\n",
99 | " tissue.name = file_prefix\n",
100 | " \n",
101 | " # https://brainweb.bic.mni.mcgill.ca/brainweb/tissue_mr_parameters.txt\n",
102 | " # J.Z. Bojorquez et al. / Magnetic Resonance Imaging 35 (2017) 69–80 \n",
103 | " # Schneck https://aapm.onlinelibrary.wiley.com/doi/epdf/10.1118/1.597854 \n",
104 | " if 'dura' in file_prefix:\n",
105 | " tissue.T1 = 1000\n",
106 | " tissue.T2 = 100\n",
107 | " tissue.T2star = 50\n",
108 | " tissue.density = 0.6\n",
109 | " tissue.susceptibility = -9.05\n",
110 | " if 'csf' in file_prefix:\n",
111 | " tissue.T1 = 4000\n",
112 | " tissue.T2 = 2000\n",
113 | " tissue.T2star = 2000\n",
114 | " tissue.density = 1.0\n",
115 | " tissue.susceptibility = -9.05\n",
116 | " if 'fat' in file_prefix:\n",
117 | " tissue.T1 = 250\n",
118 | " tissue.T2 = 70\n",
119 | " tissue.T2star = 60\n",
120 | " tissue.density = 1.0\n",
121 | " tissue.susceptibility = -7.79\n",
122 | " tissue.freq = 440 # Hz\n",
123 | " if 'bck' in file_prefix:\n",
124 | " tissue.T1 = 800\n",
125 | " tissue.T2 = 800\n",
126 | " tissue.T2star = 800\n",
127 | " tissue.density = 0.0\n",
128 | " tissue.susceptibility = 0.36\n",
129 | " if 'muscle' in file_prefix:\n",
130 | " tissue.T1 = 900\n",
131 | " tissue.T2 = 50\n",
132 | " tissue.T2star = 40\n",
133 | " tissue.density = 1\n",
134 | " tissue.susceptibility = -9.05\n",
135 | " if 'skin' in file_prefix:\n",
136 | " tissue.T1 = 2569\n",
137 | " tissue.T2 = 329\n",
138 | " tissue.T2star = 58\n",
139 | " tissue.density = 1.0\n",
140 | " tissue.susceptibility = -9.05\n",
141 | " if 'marrow' in file_prefix:\n",
142 | " tissue.T1 = 1000\n",
143 | " tissue.T2 = 50\n",
144 | " tissue.T2star = 40\n",
145 | " tissue.density = 1.0\n",
146 | " tissue.susceptibility = -9.05\n",
147 | " if 'vessels' in file_prefix:\n",
148 | " tissue.T1 = 1600\n",
149 | " tissue.T2 = 80\n",
150 | " tissue.T2star = 70\n",
151 | " tissue.density = 1.\n",
152 | " tissue.susceptibility = -9 #-6.56\n",
153 | " if 'wm' in file_prefix:\n",
154 | " tissue.T1 = 500\n",
155 | " tissue.T2 = 70\n",
156 | " tissue.T2star = 61\n",
157 | " tissue.density = 0.77\n",
158 | " tissue.susceptibility = -9.05\n",
159 | " if 'gm' in file_prefix:\n",
160 | " tissue.T1 = 900\n",
161 | " tissue.T2 = 90\n",
162 | " tissue.T2star = 69\n",
163 | " tissue.density = 0.86\n",
164 | " tissue.susceptibility = -9.05\n",
165 | " if 'skull' in file_prefix:\n",
166 | " tissue.T1 = 300\n",
167 | " tissue.T2 = 1\n",
168 | " tissue.T2star = 1\n",
169 | " tissue.density = 0.1\n",
170 | " tissue.susceptibility = -8.86\n",
171 | " \n",
172 | " print(f'{tissue.name} T1 = {tissue.T1} T2 = {tissue.T2} T2star = {tissue.T2star} density = {tissue.density} susceptibility = {tissue.susceptibility}')\n",
173 | " tissues.append(tissue) \n"
174 | ]
175 | },
176 | {
177 | "cell_type": "code",
178 | "execution_count": 24,
179 | "metadata": {},
180 | "outputs": [
181 | {
182 | "name": "stdout",
183 | "output_type": "stream",
184 | "text": [
185 | "subject54_fat_v susceptibility: -7.79 max volume: 1.0\n",
186 | "subject54_csf_v susceptibility: -9.05 max volume: 1.0\n",
187 | "subject54_gm_v susceptibility: -9.05 max volume: 1.0\n",
188 | "subject54_wm_v susceptibility: -9.05 max volume: 1.0\n",
189 | "subject54_dura_v susceptibility: -9.05 max volume: 1.0\n",
190 | "subject54_marrow_v susceptibility: -9.05 max volume: 1.0\n",
191 | "subject54_muscles_skin_v susceptibility: -9.05 max volume: 1.0\n",
192 | "subject54_skull_v susceptibility: -8.86 max volume: 1.0\n",
193 | "subject54_vessels susceptibility: -9 max volume: 1.0\n",
194 | "subject54_bck_v susceptibility: 0.36 max volume: 1.0\n",
195 | "subject54_muscles_v susceptibility: -9.05 max volume: 1.0\n",
196 | "subject54_fat2_v susceptibility: -7.79 max volume: 1.0\n"
197 | ]
198 | }
199 | ],
200 | "source": [
201 | "# Generate a 3D dipole kernel for forward QSM model\n",
202 | "def generate_3d_dipole_kernel(data_shape, voxel_size, b_vec):\n",
203 | " fov = np.array(data_shape) * np.array(voxel_size)\n",
204 | "\n",
205 | " rz, ry, rx = np.meshgrid(np.arange(-data_shape[0] // 2, data_shape[0] // 2),\n",
206 | " np.arange(-data_shape[1] // 2, data_shape[1] // 2),\n",
207 | " np.arange(-data_shape[2] // 2, data_shape[2] // 2), indexing='ij')\n",
208 | "\n",
209 | " rz, ry, rx = rz / fov[0], ry / fov[1], rx / fov[2]\n",
210 | "\n",
211 | " sq_dist = rx ** 2 + ry ** 2 + rz ** 2\n",
212 | " sq_dist[sq_dist == 0] = 1e-6\n",
213 | " d2 = ((b_vec[0] * rz + b_vec[1] * ry + b_vec[2] * rx) ** 2) / sq_dist\n",
214 | " kernel = (1 / 3 - d2)\n",
215 | "\n",
216 | " return kernel\n",
217 | "\n",
218 | "# Apply a forward convolution to the susceptibility maps in k-space\n",
219 | "def forward_convolution(chi_sample):\n",
220 | " scaling = np.sqrt(chi_sample.size)\n",
221 | " chi_fft = np.fft.fftn(chi_sample) / scaling\n",
222 | " \n",
223 | " chi_fft_t_kernel = chi_fft * np.fft.fftshift(generate_3d_dipole_kernel(chi_sample.shape, voxel_size=250.25, b_vec=[1, 0, 0]))\n",
224 | " \n",
225 | " tissue_phase = np.fft.ifftn(chi_fft_t_kernel)\n",
226 | " tissue_phase = np.real(tissue_phase * scaling)\n",
227 | "\n",
228 | " return tissue_phase\n",
229 | " \n",
230 | "# Get chi from fuzzy maps\n",
231 | "chi = np.zeros(tissues[0].volume.shape)\n",
232 | "for tissue in tissues:\n",
233 | " chi += tissue[:,:,:]*tissue.susceptibility\n",
234 | " print(f'{tissue.name} susceptibility: {tissue.susceptibility} max volume: {tissue[:,:,:].max()}')\n",
235 | " \n",
236 | "# Simulate the phase maps\n",
237 | "phs_tissue_simulated = forward_convolution(chi)\n",
238 | "\n",
239 | "# Estimate the gradient of the phase maps to estimate T2*\n",
240 | "grad = np.gradient(phs_tissue_simulated)\n",
241 | "grad = np.sqrt( grad[0]**2 + grad[1]**2 + grad[2]**2)\n",
242 | "grad = grad / np.mean(grad)"
243 | ]
244 | },
245 | {
246 | "cell_type": "code",
247 | "execution_count": 25,
248 | "metadata": {},
249 | "outputs": [],
250 | "source": [
251 | "# For interactive plotting\n",
252 | "from ipywidgets import interact, interactive, FloatSlider, IntSlider, ToggleButton\n",
253 | "from IPython.display import clear_output, display, HTML\n",
254 | "\n",
255 | "# for plotting modified style for better visualization\n",
256 | "import matplotlib.pyplot as plt \n",
257 | "import matplotlib as mpl\n",
258 | "mpl.rcParams['lines.linewidth'] = 4\n",
259 | "mpl.rcParams['axes.titlesize'] = 16\n",
260 | "mpl.rcParams['axes.labelsize'] = 14\n",
261 | "mpl.rcParams['xtick.labelsize'] = 12\n",
262 | "mpl.rcParams['ytick.labelsize'] = 12\n",
263 | "mpl.rcParams['legend.fontsize'] = 12"
264 | ]
265 | },
266 | {
267 | "cell_type": "code",
268 | "execution_count": 32,
269 | "metadata": {},
270 | "outputs": [],
271 | "source": [
272 | "# Export the Tissue objects to HDF5 files\n",
273 | "for tissue in tissues:\n",
274 | " tissue.export(f'{tissue.name}.h5')\n",
275 | "\n",
276 | "# Export gradient and phase\n",
277 | "grad = np.iinfo(np.uint16).max*grad / np.max(grad)\n",
278 | "with h5py.File('gradient.h5', 'w') as f:\n",
279 | " f.create_dataset('gradient', data=grad.astype(np.uint16), compression='gzip')\n",
280 | " #f.create_dataset('phase', data=phs_tissue_simulated, compression='gzip') \n"
281 | ]
282 | },
283 | {
284 | "cell_type": "code",
285 | "execution_count": 30,
286 | "metadata": {},
287 | "outputs": [
288 | {
289 | "data": {
290 | "application/vnd.jupyter.widget-view+json": {
291 | "model_id": "9f5964a61dcb42efae7c661b1bfec530",
292 | "version_major": 2,
293 | "version_minor": 0
294 | },
295 | "text/plain": [
296 | "interactive(children=(FloatSlider(value=50.0, description='TR [ms]', max=5000.0, min=2.0, step=1.0), FloatSlid…"
297 | ]
298 | },
299 | "metadata": {},
300 | "output_type": "display_data"
301 | }
302 | ],
303 | "source": [
304 | "# Need \n",
305 | "# Tissues\n",
306 | "# Gradient\n",
307 | "\n",
308 | "def calc_signal( TE, TR, B0, freq, alpha, M0, T1, T2, T2star, spin_echo, grad_slice):\n",
309 | " # T1, T2, T2star in ms\n",
310 | " # B0 in Hz\n",
311 | " # alpha in degrees\n",
312 | " # M0 in arbitrary units\n",
313 | " # TE, TR in ms\n",
314 | "\n",
315 | " # convert B0 to rad/s\n",
316 | " B0 = B0 * 2 * np.pi\n",
317 | "\n",
318 | " # convert alpha to rad\n",
319 | " alpha = alpha * np.pi / 180\n",
320 | "\n",
321 | " # calculate the longitudinal magnetization based on flip angle\n",
322 | " #Mxy = Mz * np.sin(alpha) * np.exp(-TE / T2star) \n",
323 | " if spin_echo:\n",
324 | " Mz180 = -M0*(1 - np.exp(-0.5*TE/T1))\n",
325 | " Mz = M0 + (Mz180 - M0) * np.exp(-(TR-0.5*TE) / T1)\n",
326 | " Mxy = Mz * np.exp(-TE / T2)\n",
327 | " else:\n",
328 | " Mz = M0 * (1 - np.exp(-TR / T1)) / (1 - np.cos(alpha) * np.exp(-TR / T1))\n",
329 | " Mxy = Mz * np.sin(alpha) * np.exp(-TE / T2star) * np.exp(-grad_slice*TE/40) * np.exp(-1j*2*np.pi*freq*TE/1000)\n",
330 | " \n",
331 | " return Mxy\n",
332 | "\n",
333 | "\n",
334 | "def plot_image(TR, TE, flip, slice, spin_echo, noise_level):\n",
335 | " \n",
336 | "\n",
337 | " #print(f'{TR}, {TE}, {flip}')\n",
338 | "\n",
339 | " signal = np.zeros((434, 362), dtype=np.complex128)\n",
340 | " for tissue in tissues:\n",
341 | " signal += calc_signal(TE, TR, 0, tissue.freq, flip, tissue[slice]*tissue.density, tissue.T1, tissue.T2, tissue.T2star, spin_echo, grad[slice])\n",
342 | "\n",
343 | " signal = np.abs( signal + np.random.normal(0, noise_level, signal.shape))\n",
344 | "\n",
345 | " plt.figure()\n",
346 | " plt.imshow(np.flip(signal), cmap='gray')\n",
347 | "\n",
348 | " #plt.imshow(np.flip(phs_tissue_simulated[slice]), cmap='gray')\n",
349 | "\n",
350 | " plt.xticks([])\n",
351 | " plt.yticks([])\n",
352 | " \n",
353 | " if spin_echo:\n",
354 | " plt.title(f' SE:flip={int(flip)},TR={int(TR)},TE={int(TE)} ')\n",
355 | " else:\n",
356 | " plt.title(f'GRE:flip={int(flip)},TR={int(TR)},TE={int(TE)} ')\n",
357 | "\n",
358 | " plt.colorbar()\n",
359 | " plt.show()\n",
360 | "\n",
361 | "\n",
362 | "\n",
363 | "TRslider = FloatSlider(min=2, max=5000, step=1, value=50, description='TR [ms]',continuous_update=True)\n",
364 | "TEslider = FloatSlider(min=1, max=50, step=1, value=3, description='TE [ms]',continuous_update=True)\n",
365 | "flip_slider = FloatSlider(min=1, max=90, step=1, value=10, description='Flip [deg.]',continuous_update=True)\n",
366 | "spin_echo_toggle = ToggleButton(value=False, description='Spin echo', continuous_update=True)\n",
367 | "\n",
368 | "def update_max_TE(change):\n",
369 | " TEslider.max = min(change['new']-1,100.0)\n",
370 | "\n",
371 | "def update_flip_min(change):\n",
372 | " \n",
373 | " if change['new']:\n",
374 | " flip_slider.max = 90\n",
375 | " flip_slider.min = 90\n",
376 | " else:\n",
377 | " flip_slider.max = 90\n",
378 | " flip_slider.min = 1\n",
379 | "\n",
380 | "\n",
381 | "TRslider.observe(update_max_TE, names='value')\n",
382 | "spin_echo_toggle.observe(update_flip_min, names='value')\n",
383 | "\n",
384 | "w = interactive(plot_image, \n",
385 | " TR=TRslider,\n",
386 | " TE=TEslider,\n",
387 | " flip=flip_slider,\n",
388 | " slice=IntSlider(min=0, max=362, step=1, value=130, description='Slice',continuous_update=True),\n",
389 | " spin_echo=spin_echo_toggle,\n",
390 | " noise_level=FloatSlider(min=0, max=0.1, step=0.001, value=0.01, description='Noise level',continuous_update=True)\n",
391 | " )\n",
392 | "display(w)"
393 | ]
394 | }
395 | ],
396 | "metadata": {
397 | "kernelspec": {
398 | "display_name": "pytorch2.1",
399 | "language": "python",
400 | "name": "python3"
401 | },
402 | "language_info": {
403 | "codemirror_mode": {
404 | "name": "ipython",
405 | "version": 3
406 | },
407 | "file_extension": ".py",
408 | "mimetype": "text/x-python",
409 | "name": "python",
410 | "nbconvert_exporter": "python",
411 | "pygments_lexer": "ipython3",
412 | "version": "3.10.13"
413 | }
414 | },
415 | "nbformat": 4,
416 | "nbformat_minor": 2
417 | }
418 |
--------------------------------------------------------------------------------
/Simulations/demodulation_example.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import matplotlib.pyplot as plt
3 |
4 | # Makeup a signal with a fast and slow component
5 | time = np.linspace(0,10,10000)
6 | s_fast = np.exp(1j*time*25*np.pi)
7 | s_slow = np.exp(1j*time*1*np.pi)*np.exp(-(time-3)/0.5)
8 | s_total = s_fast*s_slow
9 |
10 | def plot_signal(signal):
11 | if np.any(np.iscomplex(signal)):
12 | plt.plot(time, np.real(signal))
13 | plt.plot(time, np.imag(signal))
14 | plt.plot(time, np.abs(signal),'k')
15 | plt.legend(('Real', 'Imag', 'Mag'))
16 | else:
17 | plt.plot(time, signal,'k')
18 |
19 | plt.xlim(3,4)
20 | plt.ylim(-1.1,1.1)
21 |
22 |
23 | # Plot the fast B0 modulation
24 | plt.figure()
25 | plot_signal(s_fast)
26 | plt.title('B_0 Modulation')
27 | plt.xlabel("Time")
28 | plt.ylabel("Signal")
29 | plt.show()
30 |
31 | # Plot the slow modulation
32 | plt.figure()
33 | plot_signal(s_slow)
34 | plt.title('Rotating Frame Signal')
35 | plt.xlabel("Time")
36 | plt.ylabel("Signal")
37 | plt.show()
38 |
39 | plt.figure()
40 | plot_signal(s_total)
41 | plt.title('Lab Frame Signal')
42 | plt.xlabel("Time")
43 | plt.ylabel("Signal")
44 | plt.show()
45 |
46 | plt.figure()
47 | plot_signal(np.real(s_total))
48 | plt.xlabel("Time")
49 | plt.ylabel("Voltage")
50 | plt.show()
51 |
52 |
53 |
54 | plt.figure()
55 | s_mod = np.real(s_total)*s_fast
56 | plot_signal(s_mod)
57 | plt.xlabel("Time")
58 | plt.ylabel("Signal")
59 | plt.show()
60 |
61 | plt.figure()
62 | s_mod = np.real(s_total)*np.conj(s_fast)
63 | x = np.linspace(-1,1,201)
64 | filter = np.exp( -x**2/0.25)
65 | filter = 2*filter / np.sum(filter)
66 | s_mod = np.convolve(s_mod,filter,'same')
67 | plot_signal(s_mod)
68 | plt.xlabel("Time")
69 | plt.ylabel("Signal")
70 | plt.show()
71 |
72 |
--------------------------------------------------------------------------------
/Simulations/epi_distortions.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import matplotlib.pyplot as plt
3 | import math
4 | #subprocess.call('git clone https://github.com/mikgroup/sigpy.git')
5 | #subprocess.call('pip install sigpy')
6 | import sigpy as sp
7 | import sigpy.mri
8 | import matplotlib.animation as manimation
9 | import time
10 |
11 | # Simulation paramaters
12 | Tpe = 0.1*1e-3 # Time for blipped PE
13 | Tread = 1.0*1e-3 # Readout time in ms
14 | Npe = 128
15 | Nfreq = 128
16 | t_offset = 0.0
17 |
18 | t = []
19 | kx = []
20 | ky = []
21 |
22 | k_line = np.linspace(-Nfreq/2.0,Nfreq/2.0,Nfreq+2)
23 | ky_all = np.linspace(-Npe/2.0,Npe/2.0,Npe)
24 | t_line = np.linspace(0, Tread,Nfreq+2)
25 |
26 | for pe in range(Npe):
27 | if pe%2 == 0:
28 | if pe > 0:
29 | kx = np.concatenate([kx, k_line])
30 | else:
31 | kx = k_line
32 | else:
33 | if pe > 0:
34 | kx = np.concatenate([kx, -k_line])
35 | else:
36 | kx = -k_line
37 | ky = np.concatenate([ky,ky_all[pe]*np.ones(k_line.shape,k_line.dtype)])
38 | t = np.concatenate([t,t_line+t_offset])
39 |
40 | t_offset += Tpe + Tread
41 |
42 | ky = -ky
43 |
44 | # Plot Kx,Ky
45 | plt.figure()
46 | plt.plot(kx, ky)
47 |
48 | # Kx/Ky vs time
49 | plt.figure()
50 | plt.plot(t, kx)
51 | plt.plot(t, ky)
52 |
53 | # Define fat as the outer ring
54 | sl_amps = [1.0,-1.0]
55 | sl_scales = [[.6900, .920, .810], # white big
56 | [.6624, .874, .780]] # gray big
57 | sl_offsets = [[0., 0., 0],
58 | [0., -.0184, 0]]
59 | sl_angles = [[0, 0, 0],
60 | [0, 0, 0]]
61 | fat = sp.sim.phantom([256,256], sl_amps, sl_scales, sl_offsets, sl_angles,dtype=np.complex64)
62 |
63 |
64 | plt.figure()
65 | plt.imshow(np.abs(fat))
66 |
67 |
68 | sl_amps = [0.2, 1., 1.2, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1]
69 |
70 | sl_scales = [[.6624, .874, .780], # gray big
71 | [.1100, .310, .220], # right black
72 | [.1600, .410, .280], # left black
73 | [.2100, .250, .410], # gray center blob
74 | [.0460, .046, .050],
75 | [.0460, .046, .050],
76 | [.0460, .046, .050], # left small dot
77 | [.0230, .023, .020], # mid small dot
78 | [.0230, .023, .020]]
79 |
80 | sl_offsets = [[0., -.0184, 0],
81 | [.22, 0., 0],
82 | [-.22, 0., 0],
83 | [0., .35, -.15],
84 | [0., .1, .25],
85 | [0., -.1, .25],
86 | [-.08, -.605, 0],
87 | [0., -.606, 0],
88 | [.06, -.605, 0]]
89 |
90 | sl_angles = [[0, 0, 0],
91 | [-18, 0, 10],
92 | [18, 0, 10],
93 | [0, 0, 0],
94 | [0, 0, 0],
95 | [0, 0, 0],
96 | [0, 0, 0],
97 | [0, 0, 0],
98 | [0, 0, 0]]
99 |
100 |
101 | # Get complex phantom
102 | water = sp.sim.phantom([256,256], sl_amps, sl_scales, sl_offsets, sl_angles,dtype=np.complex64)
103 | [x,y] = np.meshgrid(np.linspace(-0.5,0.5,256),np.linspace(-0.5,0.5,256))
104 | x0 = 0.0
105 | y0 = 0.4
106 | wY = 0.3
107 | wX = 0.2
108 | offmap = np.exp( -((x-x0)**2/(wX**2) + (y-y0)**2/(wY**2))**2)
109 |
110 | # Subtract off mean
111 | offmap -= np.mean(offmap)
112 | offmap /= np.max(offmap)
113 |
114 | offmap = np.flipud(offmap)
115 | water = np.flipud(water)
116 | fat = np.flipud(fat)
117 |
118 | # Create gradients as triangles
119 | plt.figure()
120 | plt.imshow(offmap)
121 | plt.draw()
122 | plt.colorbar()
123 |
124 | FFMpegWriter = manimation.writers['ffmpeg']
125 | metadata = dict(title='epi_without_fat', artist='KMJ',
126 | comment='EPI simulation with off-resonance')
127 | writer = FFMpegWriter(fps=10, metadata=metadata)
128 |
129 | # Do this on GPU
130 | device = sp.backend.Device(0)
131 | xp = device.xp
132 | kx = sp.backend.to_device(kx, device)
133 | ky = sp.backend.to_device(ky, device)
134 | t = sp.backend.to_device(t, device)
135 | water = sp.backend.to_device(water, device)
136 | fat = sp.backend.to_device(fat, device)
137 | offmap = sp.backend.to_device(offmap, device)
138 |
139 | fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(20, 10))
140 |
141 | with writer.saving(fig, "epi_without_fat.mp4", 100):
142 | for fmax in xp.linspace(-200.0,200.0,41):
143 |
144 | with device:
145 |
146 | t_start = time.time()
147 | s = xp.zeros(kx.shape, xp.complex64)
148 | img_est = xp.zeros(water.shape, xp.complex64)
149 |
150 | # Now do the DFT
151 | [x, y] = xp.meshgrid(xp.linspace(-0.5, 0.5, 256), xp.linspace(-0.5, 0.5, 256))
152 | for pos in range(kx.shape[0]):
153 | Gphase = xp.exp(1j*2.0*math.pi*(kx[pos]*x + ky[pos]*y))
154 | Ophase = xp.exp(1j*2.0*math.pi*t[pos]*offmap*fmax)
155 | s[pos] = xp.sum((water+0.0*fat*xp.exp(2j*math.pi*t[pos]*440))*Gphase*Ophase)
156 |
157 | fr = 0.9*Nfreq/2.0
158 | fw = 0.1*Nfreq/2.0
159 |
160 | kr = xp.abs(kx[pos])
161 | wx = 1. / (1. + xp.exp( (kr-fr)/fw))
162 | kr = xp.abs(ky[pos])
163 | wy = 1. / (1. + xp.exp( (kr-fr)/fw))
164 |
165 | img_est += s[pos]*xp.conj(Gphase)*wx*wy
166 | print(f'Took {time.time()-t_start}')
167 |
168 | # Get Images
169 | img_est_cpu = sp.backend.to_device(img_est,sp.cpu_device)
170 | offmap_cpu = sp.backend.to_device(offmap,sp.cpu_device)
171 | fmax_cpu = sp.backend.to_device(fmax,sp.cpu_device)
172 |
173 | from mpl_toolkits.axes_grid1 import make_axes_locatable
174 | def colorbar(mappable):
175 | ax = mappable.axes
176 | fig = ax.figure
177 | divider = make_axes_locatable(ax)
178 | cax = divider.append_axes("right", size="5%", pad=0.05)
179 | return fig.colorbar(mappable, cax=cax)
180 |
181 | img1 = ax1.imshow(fmax_cpu*offmap_cpu,cmap='Spectral')
182 | img1.set_clim(-150,150)
183 | colorbar(img1)
184 | ax1.set_title('Off Resonance [Hz]')
185 | ax1.axis('off')
186 |
187 |
188 | img2 = ax2.imshow(np.abs(img_est_cpu),cmap='gray')
189 | ax2.set_title('Estimated Image')
190 | ax2.axis('off')
191 |
192 | plt.tight_layout(h_pad=1)
193 | plt.draw()
194 | plt.pause(0.0001)
195 | writer.grab_frame()
196 |
197 |
198 | plt.show()
199 |
200 |
201 |
202 |
203 |
204 |
--------------------------------------------------------------------------------
/Simulations/spiral_distortions.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import subprocess
3 | import matplotlib.pyplot as plt
4 | import math
5 | #subprocess.call('git clone https://github.com/mikgroup/sigpy.git')
6 | #subprocess.call('pip install sigpy')
7 | import sigpy as sp
8 | import sigpy.mri
9 | import matplotlib.animation as manimation
10 |
11 | Tpe = 0.1*1e-3 # Time for blipped PE
12 | Tread = 30.0*1e-3 # Readout time in ms
13 | Npe = 128
14 | Nfreq = 128
15 | t_offset = 0.0
16 |
17 | t = []
18 | kx = []
19 | ky = []
20 |
21 | t = np.linspace(0, Tread,Nfreq*Nfreq)
22 | tt = np.sqrt(t/Tread)
23 | kx = Nfreq/2*tt*np.cos( 2*math.pi*Nfreq/2*tt)
24 | ky = Nfreq/2*tt*np.sin( 2*math.pi*Nfreq/2*tt)
25 |
26 | #kx,ky = -ky,kx
27 | ky = -ky
28 |
29 | # Create gradients as triangles
30 | plt.figure()
31 | plt.plot(kx,ky)
32 | plt.show()
33 |
34 | # Create gradients as triangles
35 | plt.figure()
36 | plt.plot(t,kx)
37 | plt.plot(t,ky)
38 |
39 | sl_amps = [1.0,-1.0]
40 | sl_scales = [[.6900, .920, .810], # white big
41 | [.6624, .874, .780]] # gray big
42 | sl_offsets = [[0., 0., 0],
43 | [0., -.0184, 0]]
44 | sl_angles = [[0, 0, 0],
45 | [0, 0, 0]]
46 | fat = sp.sim.phantom([256,256], sl_amps, sl_scales, sl_offsets, sl_angles,dtype=np.complex64)
47 |
48 |
49 | plt.figure()
50 | plt.imshow(np.abs(fat))
51 |
52 |
53 | sl_amps = [0.2, 1., 1.2, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1]
54 |
55 | sl_scales = [[.6624, .874, .780], # gray big
56 | [.1100, .310, .220], # right black
57 | [.1600, .410, .280], # left black
58 | [.2100, .250, .410], # gray center blob
59 | [.0460, .046, .050],
60 | [.0460, .046, .050],
61 | [.0460, .046, .050], # left small dot
62 | [.0230, .023, .020], # mid small dot
63 | [.0230, .023, .020]]
64 |
65 | sl_offsets = [[0., -.0184, 0],
66 | [.22, 0., 0],
67 | [-.22, 0., 0],
68 | [0., .35, -.15],
69 | [0., .1, .25],
70 | [0., -.1, .25],
71 | [-.08, -.605, 0],
72 | [0., -.606, 0],
73 | [.06, -.605, 0]]
74 |
75 | sl_angles = [[0, 0, 0],
76 | [-18, 0, 10],
77 | [18, 0, 10],
78 | [0, 0, 0],
79 | [0, 0, 0],
80 | [0, 0, 0],
81 | [0, 0, 0],
82 | [0, 0, 0],
83 | [0, 0, 0]]
84 |
85 |
86 | # Get complex phantom
87 | water = sp.sim.phantom([256,256], sl_amps, sl_scales, sl_offsets, sl_angles,dtype=np.complex64)
88 | [x,y] = np.meshgrid(np.linspace(-0.5,0.5,256),np.linspace(-0.5,0.5,256))
89 | x0 = 0.0
90 | y0 = 0.4
91 | wY = 0.3
92 | wX = 0.2
93 | offmap = np.exp( -((x-x0)**2/(wX**2) + (y-y0)**2/(wY**2))**2)
94 |
95 | # Subtract off mean
96 | offmap -= np.mean(offmap)
97 | offmap /= np.max(offmap)
98 |
99 | offmap = np.flipud(offmap)
100 | water = np.flipud(water)
101 | fat = np.flipud(fat)
102 |
103 |
104 | # Create gradients as triangles
105 | plt.figure()
106 | plt.imshow(offmap)
107 | plt.draw()
108 | plt.colorbar()
109 |
110 |
111 | FFMpegWriter = manimation.writers['ffmpeg']
112 | metadata = dict(title='spiral_without_fat', artist='KMJ',
113 | comment='EPI simulation with off-resonance')
114 | writer = FFMpegWriter(fps=10, metadata=metadata)
115 |
116 | # Do this on GPU
117 | device = sp.backend.Device(0)
118 | xp = device.xp
119 | kx = sp.backend.to_device(kx,device)
120 | ky = sp.backend.to_device(ky,device)
121 | t = sp.backend.to_device(t,device)
122 | water = sp.backend.to_device(water,device)
123 | fat = sp.backend.to_device(fat,device)
124 |
125 | offmap = sp.backend.to_device(offmap,device)
126 |
127 | fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(20, 10))
128 |
129 | with writer.saving(fig, "spiral_without_fat.mp4", 100):
130 | for fmax in xp.linspace(-200.0,200.0,41):
131 |
132 | print(fmax)
133 | with device:
134 |
135 | s = xp.zeros(kx.shape, xp.complex64)
136 | img_est = xp.zeros(water.shape, xp.complex64)
137 |
138 | # Now do the DFT
139 | [x,y] = xp.meshgrid(xp.linspace(-0.5,0.5,256),xp.linspace(-0.5,0.5,256))
140 | for pos in range(kx.shape[0]):
141 | Gphase = xp.exp(1j*2.0*math.pi*(kx[pos]*x + ky[pos]*y))
142 | Ophase = xp.exp(1j*2.0*math.pi*t[pos]*offmap*fmax)
143 | s[pos] = xp.sum((water+0.0*fat*xp.exp(2j*math.pi*t[pos]*440))*Gphase*Ophase)
144 |
145 | fr = 0.9*Nfreq/2.0
146 | fw = 0.1*Nfreq/2.0
147 |
148 | kr = xp.abs(kx[pos])
149 | wx = 1. / (1. + xp.exp( (kr-fr)/fw))
150 | kr = xp.abs(ky[pos])
151 | wy = 1. / (1. + xp.exp( (kr-fr)/fw))
152 |
153 | img_est += s[pos]*xp.conj(Gphase)*wx*wy
154 |
155 |
156 | #s = sp.backend.to_device(s,sp.cpu_device)
157 | #s = np.reshape(s,[Npe,Nfreq+2])
158 |
159 | # Get Images
160 | img_est_cpu = sp.backend.to_device(img_est,sp.cpu_device)
161 | offmap_cpu = sp.backend.to_device(offmap,sp.cpu_device)
162 | fmax_cpu = sp.backend.to_device(fmax,sp.cpu_device)
163 |
164 | from mpl_toolkits.axes_grid1 import make_axes_locatable
165 | def colorbar(mappable):
166 | ax = mappable.axes
167 | fig = ax.figure
168 | divider = make_axes_locatable(ax)
169 | cax = divider.append_axes("right", size="5%", pad=0.05)
170 | return fig.colorbar(mappable, cax=cax)
171 |
172 | img1 = ax1.imshow(fmax_cpu*offmap_cpu,cmap='Spectral')
173 | img1.set_clim(-150,150)
174 | colorbar(img1)
175 | ax1.set_title('Off Resonance [Hz]')
176 | ax1.axis('off')
177 |
178 |
179 | img2 = ax2.imshow(np.abs(img_est_cpu),cmap='gray')
180 | ax2.set_title('Estimated Image')
181 | ax2.axis('off')
182 |
183 | plt.tight_layout(h_pad=1)
184 | plt.draw()
185 | plt.pause(0.0001)
186 | writer.grab_frame()
187 |
188 |
189 | plt.show()
190 |
191 |
192 |
193 |
194 |
195 |
--------------------------------------------------------------------------------