diff --git a/.DS_Store b/.DS_Store
index 7239436..a1b93e9 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/05-B0/README.md b/05-B0/README.md
new file mode 100644
index 0000000..31287b9
--- /dev/null
+++ b/05-B0/README.md
@@ -0,0 +1,2 @@
+# mooc_b0_mapping
+Notebooks to create Figures for the B0 mapping chapter of the MOOC
diff --git a/05-B0/binder/postBuild b/05-B0/binder/postBuild
new file mode 100644
index 0000000..e69de29
diff --git a/05-B0/binder/requirements.txt b/05-B0/binder/requirements.txt
new file mode 100644
index 0000000..e13b1a0
--- /dev/null
+++ b/05-B0/binder/requirements.txt
@@ -0,0 +1,7 @@
+jupyter-book==0.13.0
+lxml_html_clean
+nibabel
+pandas
+plotly
+scipy
+tabulate
diff --git a/05-B0/binder/runtime.txt b/05-B0/binder/runtime.txt
new file mode 100644
index 0000000..3cd3102
--- /dev/null
+++ b/05-B0/binder/runtime.txt
@@ -0,0 +1 @@
+python-3.8
\ No newline at end of file
diff --git a/05-B0/content/_config.yml b/05-B0/content/_config.yml
new file mode 100644
index 0000000..b156fa5
--- /dev/null
+++ b/05-B0/content/_config.yml
@@ -0,0 +1,37 @@
+# Book settings
+# Learn more at https://jupyterbook.org/customize/config.html
+
+title: B0 mapping
+logo: logo.png
+
+# Force re-execution of notebooks on each build.
+# See https://jupyterbook.org/content/execute.html
+execute:
+ execute_notebooks: force
+
+launch_buttons:
+ notebook_interface: "jupyterlab"
+
+# Information about where the book exists on the web
+repository:
+ url: https://github.com/qMRLab/qMRI-MOOC_B0-module # Online location of your book
+ path_to_book: content # Optional path to your book, relative to the repository root
+ branch: main # Which branch of the repository should be used when creating links (optional)
+
+only_build_toc_files: true
+
+execute:
+ timeout: 600
+
+sphinx:
+ local_extensions : # A list of local extensions to load by sphinx specified by "name: path" items
+ recursive_update : false # A boolean indicating whether to overwrite the Sphinx config (true) or recursively update
+ config : # key-value pairs to directly over-ride the Sphinx configuration
+ bibtex_reference_style: author_year
+ bibtex_default_style: plain
+ bibtex_tooltips: true
+ html_static_path: ['_static']
+ html_css_files: ['custom.css']
+
+bibtex_bibfiles:
+ - ../paper.bib
diff --git a/05-B0/content/_static/custom.css b/05-B0/content/_static/custom.css
new file mode 100644
index 0000000..905f567
--- /dev/null
+++ b/05-B0/content/_static/custom.css
@@ -0,0 +1,52 @@
+p {
+ text-align: justify;
+ }
+ a {
+ color: #870000!important;
+ }
+ a:hover {
+ color: brick!important;
+ }
+ a:visited {
+ color: #6f42c1!important;
+ }
+ body, h1, h2, h3, h4, h5 {
+ font-family: 'STIX Two Text';
+ }
+ h1 {
+ color: #342727!important;
+ }
+ .caption {
+ text-align:justify;
+ line-height:1.25;
+ font-size:80%
+ }
+
+ #site-navigation {
+ display: none;
+ }
+
+ #main-content{
+ margin: 0 auto;
+ }
+
+
+ .fa, .far, .fas {
+ color: red;
+ }
+
+ .header-article::after{
+ content: url('https://neurolibre.org/assets/joss2-b7ea8dd1f24ce2b0825db17e3495576253ba4c64df3384a66354713d76c40a75.svg');
+ width: 150px;
+ height: 80px;
+ position: relative;
+ right: 10px;
+ }
+
+ .header-article::before{
+ content: url('https://raw.githubusercontent.com/neurolibre/brand/b5e10e051a0059824ca2f4204e1288927e551d49/svg/neurolibre_logo.svg');
+ width: 33px;
+ height: 33px;
+ position: absolute;
+ left: 10px;
+ }
\ No newline at end of file
diff --git a/05-B0/content/_toc.yml b/05-B0/content/_toc.yml
new file mode 100644
index 0000000..bb3e2d9
--- /dev/null
+++ b/05-B0/content/_toc.yml
@@ -0,0 +1,5 @@
+# Table of contents
+# Learn more at https://jupyterbook.org/customize/toc.html
+
+format: jb-article
+root: paper
diff --git a/05-B0/content/assets/before_after.css b/05-B0/content/assets/before_after.css
new file mode 100644
index 0000000..a764c37
--- /dev/null
+++ b/05-B0/content/assets/before_after.css
@@ -0,0 +1,50 @@
+.beforeAfter {
+ border-radius: 5px;
+ /* border: 2px solid silver; */
+ position: relative;
+ margin: 0;
+ padding: 0;
+}
+
+.after {
+ height:100%;
+ width:100%;
+ z-index: 1;
+ position: absolute;
+ }
+
+.before {
+ height: 100%;
+ width: 50%;
+ overflow: hidden;
+ border-right: 4px solid silver;
+ z-index: 40;
+ position: absolute;
+}
+
+.middle {
+ top: 45%;
+ left: 48%;
+ z-index: 50;
+ draggable: true;
+ position: absolute;
+}
+
+.middle:before {
+ content: '\f1de';
+ font-family: "Font Awesome 6 Free";
+ font-size: 30pt;
+ font-weight: 900;
+ background-color: silver;
+ border: 1pt solid black;
+ cursor: grab;
+ border-radius: 5pt;
+}
+
+.middle:hover:before {
+ filter: brightness(1.15)
+}
+
+.middle:active:before {
+ cursor: grabbing;
+}
\ No newline at end of file
diff --git a/05-B0/content/assets/before_after.js b/05-B0/content/assets/before_after.js
new file mode 100644
index 0000000..961f5b3
--- /dev/null
+++ b/05-B0/content/assets/before_after.js
@@ -0,0 +1,57 @@
+function beforeAfter() {
+ $(".middle").on("mousedown", function () {
+ $(document).on("mouseup",function() {
+ $(document).unbind("mousemove")
+ $(document).unbind("mouseup")
+ oldX = undefined
+ oldY = undefined
+ })
+ $(document).on("mousemove", function () {
+ adjustSize()
+ })
+ })
+
+ $(".before .modebar").css({'left':'0px', 'position':'absolute'})
+}
+
+window.fetch = new Proxy(window.fetch, {
+ apply(fetch, that, args) {
+ // Forward function call to the original fetch
+ const result = fetch.apply(that, args);
+
+ // Do whatever you want with the resulting Promise
+ result.then((response) => {
+ if (args[0] == '/_dash-update-component') {
+ setTimeout(function() {beforeAfter()}, 500)
+ }})
+ return result
+ }
+ }
+ )
+
+var oldX;
+var oldY;
+function adjustSize() {
+ if (event.screenX != 0) {
+ var clientX = event.clientX
+ var scrollLeft = $(document).scrollLeft()
+
+ if (oldX && !((parseFloat($('.before').css("width"))+((clientX-scrollLeft)-oldX)) > $('.beforeAfter').width())) {
+ var width = parseFloat($('.before').css("width"))+((clientX-scrollLeft)-oldX)
+ $('.before').css("width", width)
+ $('.middle').css("left", ($('.before').width()/$('.beforeAfter').width()*100)-2 + '%')
+
+ }
+
+ if (parseFloat($('.middle').css('left'))> $('.beforeAfter').width()) {
+ $('.middle').css("left", '99.5%')
+ $('.before').css("width", $('.beforeAfter').width())
+ }
+ }
+ oldX = clientX-scrollLeft
+ event.target.style.cursor = 'grab';
+}
+
+$(document).ready(function() {
+ setTimeout(function() {beforeAfter()}, 1000)
+})
\ No newline at end of file
diff --git a/05-B0/content/chapter4_1.ipynb b/05-B0/content/chapter4_1.ipynb
new file mode 100644
index 0000000..4fc12d0
--- /dev/null
+++ b/05-B0/content/chapter4_1.ipynb
@@ -0,0 +1,516 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1fbfb83d-7790-47df-a66e-e92e64013190",
+ "metadata": {
+ "editable": true,
+ "slideshow": {
+ "slide_type": ""
+ },
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "import math\n",
+ "import json\n",
+ "import nibabel as nib\n",
+ "import numpy as np\n",
+ "from numpy.fft import ifftn, fftn, ifft, fftshift, ifftshift\n",
+ "import os\n",
+ "import plotly.express as px\n",
+ "import plotly.graph_objects as go\n",
+ "from plotly.subplots import make_subplots\n",
+ "from scipy.signal import butter, lfilter, freqz, filtfilt\n",
+ "from scipy.io import loadmat\n",
+ "import warnings\n",
+ "PI_UNICODE = \"\\U0001D70B\"\n",
+ "CHI_UNICODE = \"\\U0001D712\"\n",
+ "MICRO_UNICODE = \"\\u00B5\"\n",
+ "GYRO_BAR_RATIO_H = 42.6e6 # [Hz/T]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6aab6266-6443-4118-b61b-2dc6b796856b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fname_mag_e1 = os.path.join(\"fmap\", \"sub-fmap_magnitude1.nii.gz\")\n",
+ "fname_mag_e1_json = os.path.join(\"fmap\", \"sub-fmap_magnitude1.json\")\n",
+ "fname_phase_e1 = os.path.join(\"fmap\", \"sub-fmap_phase1.nii.gz\")\n",
+ "fname_phase_e2 = os.path.join(\"fmap\", \"sub-fmap_phase2.nii.gz\")\n",
+ "fname_mask = os.path.join(\"fmap\", \"mask.nii.gz\")\n",
+ "fname_fmap = os.path.join(\"fmap\", \"fmap.nii.gz\")\n",
+ "mag_e1 = nib.load(fname_mag_e1).get_fdata()[30:-30,8:108,30]\n",
+ "phase_e1 = nib.load(fname_phase_e1).get_fdata()[30:-30,8:108,30]\n",
+ "phase_e2 = nib.load(fname_phase_e2).get_fdata()[30:-30,8:108,30]\n",
+ "fmap_hz = nib.load(fname_fmap).get_fdata()[30:-30,8:108,30]\n",
+ "# mag_e1 = nib.load(fname_mag_e1).get_fdata()\n",
+ "# phase_e1 = nib.load(fname_phase_e1).get_fdata()\n",
+ "# phase_e2 = nib.load(fname_phase_e2).get_fdata()\n",
+ "# mask = nib.load(fname_mask).get_fdata()\n",
+ "# fmap_hz = nib.load(fname_fmap).get_fdata()\n",
+ "\n",
+ "with open(fname_mag_e1_json, 'r') as json_data:\n",
+ " data = json.load(json_data)\n",
+ "\n",
+ "freq = data[\"ImagingFrequency\"]\n",
+ "fmap_t = fmap_hz / GYRO_BAR_RATIO_H * 1e6\n",
+ "fmap_ppm = fmap_hz / freq\n",
+ "\n",
+ "fig = go.Figure()\n",
+ "fig.add_trace(go.Heatmap(z=np.rot90(mag_e1, k=-1),\n",
+ " colorscale='gray',\n",
+ " colorbar=dict(\n",
+ " title=\"a.u.\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\"\n",
+ " ))\n",
+ " )\n",
+ "fig.add_trace(go.Heatmap(z=np.rot90(phase_e2 / 4095 * 2 * math.pi - math.pi, k=-1),\n",
+ " colorscale='gray',\n",
+ " colorbar=dict(\n",
+ " title=\"Rad\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\",\n",
+ " tickvals=[-math.pi, 0, math.pi-0.01],\n",
+ " ticktext = [f\"-{PI_UNICODE}\", 0, f'{PI_UNICODE}']\n",
+ " ),\n",
+ " visible=False))\n",
+ "fig.add_trace(go.Heatmap(z=np.rot90(fmap_hz, k=-1),\n",
+ " colorscale='gray',\n",
+ " colorbar=dict(\n",
+ " title=\"Hz\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\"\n",
+ " ),\n",
+ " visible=False))\n",
+ "fig.add_trace(go.Heatmap(z=np.rot90(fmap_t, k=-1),\n",
+ " colorscale='gray',\n",
+ " colorbar=dict(\n",
+ " title=f\"{MICRO_UNICODE}T\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\"\n",
+ " ),\n",
+ " visible=False))\n",
+ "fig.add_trace(go.Heatmap(z=np.rot90(fmap_ppm, k=-1),\n",
+ " colorscale='gray',\n",
+ " colorbar=dict(\n",
+ " title=\"ppm\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\"\n",
+ " ),\n",
+ " visible=False))\n",
+ "\n",
+ "\n",
+ "\n",
+ "x0=0\n",
+ "y0=89\n",
+ "x1=10\n",
+ "y1=99\n",
+ "h = 2\n",
+ "rounded_bottom_left = f' M {x0+h}, {y0} Q {x0}, {y0} {x0}, {y0+h}'#\n",
+ "rounded_top_left = f' L {x0}, {y1-h} Q {x0}, {y1} {x0+h}, {y1}'\n",
+ "rounded_top_right = f' L {x1-h}, {y1} Q {x1}, {y1} {x1}, {y1-h}'\n",
+ "rounded_bottom_right = f' L {x1}, {y0+h} Q {x1}, {y0} {x1-h}, {y0}Z'\n",
+ "path = rounded_bottom_left + rounded_top_left+\\\n",
+ " rounded_top_right+rounded_bottom_right\n",
+ "\n",
+ "annotations = ['A', 'B', 'C', 'D', 'E']\n",
+ "shapes = []\n",
+ "for i_shape, annotation in enumerate(annotations):\n",
+ " shapes.append(dict(type='path',\n",
+ " path=path,\n",
+ " fillcolor='white',\n",
+ " layer='above',\n",
+ " line=dict(width=1),\n",
+ " label=dict(text=f\"{annotation}\")\n",
+ " )\n",
+ " )\n",
+ "\n",
+ "fig.add_shape(shapes[0])\n",
+ "# Add dropdown\n",
+ "fig.update_layout(\n",
+ " title_text=\"Magnitude\",\n",
+ " title_x=0.5,\n",
+ " height=500,\n",
+ " width=600,\n",
+ " updatemenus=[\n",
+ " dict(\n",
+ " buttons=list([\n",
+ " dict(\n",
+ " method=\"update\",\n",
+ " args=[{\"visible\": [True, False, False, False, False]},\n",
+ " {'shapes': [shapes[0]], \"title\": \"Magnitude\"}],\n",
+ " label=\"Magnitude\",\n",
+ " ),\n",
+ " dict(\n",
+ " method=\"update\",\n",
+ " args=[{\"visible\": [False, True, False, False, False]},\n",
+ " {'shapes': [shapes[1]], \"title\": \"Phase\"}],\n",
+ " label=\"Phase\",\n",
+ " \n",
+ " ),\n",
+ " dict(\n",
+ " method=\"update\",\n",
+ " args=[{\"visible\": [False, False, True, False, False]},\n",
+ " {'shapes': [shapes[2]], \"title\": \"B0 Fieldmap (Hz)\"}],\n",
+ " label=\"B0 field map (Hz)\",\n",
+ " ),\n",
+ " dict(\n",
+ " method=\"update\",\n",
+ " args=[{\"visible\": [False, False, False, True, False]},\n",
+ " {'shapes': [shapes[3]], \"title\": f\"B0 Fieldmap ({MICRO_UNICODE}Tesla)\"}],\n",
+ " label=f\"B0 field map ({MICRO_UNICODE}Tesla)\",\n",
+ " ),\n",
+ " dict(\n",
+ " method=\"update\",\n",
+ " args=[{\"visible\": [False, False, False, False, True]},\n",
+ " {'shapes': [shapes[4]], \"title\": \"B0 Fieldmap (ppm)\"}],\n",
+ " label=\"B0 field map (ppm)\",\n",
+ " )\n",
+ " ]),\n",
+ " direction=\"down\",\n",
+ " showactive=True,\n",
+ " )\n",
+ " ]\n",
+ ")\n",
+ "fig.update_xaxes(showticklabels=False)\n",
+ "fig.update_yaxes(showticklabels=False)\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "321d843b-3df3-47c1-b084-3e895f23ac0d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def dipole_kernel(b0_dir, voxel_size, n_voxels):\n",
+ " \"\"\" Create a dipole kernel\n",
+ " dipole kernel: (3*cos(theta)**2 - 1) / (4*pi*r**3)\n",
+ " => (3*r**2*cos(theta)**2 - r**2) / (4*pi*r**5)\n",
+ " => (3*b0_dir**2 - r**2) / (4*pi*r**2**2.5)\n",
+ "\n",
+ " Function inspired and derived from: https://onlinelibrary.wiley.com/doi/10.1002/mrm.28716\n",
+ " \"\"\"\n",
+ " eps = 0.00001\n",
+ " x, y, z = np.meshgrid(range(round(-n_voxels[0]/2+0.5), round(n_voxels[0]/2+0.5)), range(round(-n_voxels[1]/2+0.5), round(n_voxels[1]/2+0.5)), range(round(-n_voxels[2]/2+0.5), round(n_voxels[2]/2+0.5)), indexing='ij')\n",
+ "\n",
+ " x = x * voxel_size[0] + eps\n",
+ " y = y * voxel_size[1] + eps\n",
+ " z = z * voxel_size[2] + eps\n",
+ "\n",
+ " r2 = (x**2 + y**2 + z**2)\n",
+ "\n",
+ " d = np.prod(voxel_size) * ( 3 * ((x*b0_dir[0] + y*b0_dir[1] + z*b0_dir[2])**2) - r2 ) / (4 * math.pi * r2**2.5)\n",
+ "\n",
+ " d[np.isnan(d)] = eps\n",
+ " D = np.real(fftshift(fftn(ifftshift(d))))\n",
+ "\n",
+ " mid_voxel = n_voxels[0]//2\n",
+ " return d[n_voxels[1]//2], D[n_voxels[1]//2]\n",
+ "\n",
+ "b0_dir = (0, 0, 1)\n",
+ "voxel_size = np.array((1, 1, 1)) * 1e-3\n",
+ "n_voxels = (201,201,201)\n",
+ "d, D = dipole_kernel(b0_dir, voxel_size, n_voxels)\n",
+ "\n",
+ "fig = go.Figure()\n",
+ "fig = make_subplots(rows=1, cols=2, shared_xaxes=False, horizontal_spacing=0.13, vertical_spacing = 0.12, subplot_titles=(\"Dipole Kernel (d)\", \"Dipole Kernel (D)\"), specs=[[{\"type\": \"Heatmap\"}, {\"type\": \"Heatmap\"}]])\n",
+ "fig.add_trace(go.Heatmap(z=d, colorscale='gray', showscale=False, zmin=-1e-6, zmax=1e-6))\n",
+ "fig.add_trace(go.Heatmap(z=D, colorscale='gray', showscale=False), 1, 2)\n",
+ "fig.update_xaxes(showticklabels=False)\n",
+ "fig.update_yaxes(showticklabels=False)\n",
+ "fig.update_layout(\n",
+ " height=500,\n",
+ " width=900)\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fcc2c8b3-3e90-4c98-b8f3-9ad58138ba0a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Load cylinder (Y 64)\n",
+ "susc = nib.load(os.path.join(\"field_simulations\", \"cylinder\", \"Chi.nii.gz\")).get_fdata()\n",
+ "fmap_hz_all = nib.load(os.path.join(\"field_simulations\", \"cylinder\", \"fmap_hz.nii.gz\")).get_fdata()\n",
+ "local_field_cyl = nib.load(os.path.join(\"field_simulations\", \"cylinder\", \"local_field.nii.gz\")).get_fdata()\n",
+ "\n",
+ "# Load brain (Z 210)\n",
+ "susc_brain = nib.load(os.path.join(\"field_simulations\", \"brain\", \"chi_masked.nii.gz\")).get_fdata()\n",
+ "fmap_hz_brain_all = nib.load(os.path.join(\"field_simulations\", \"brain\", \"fmap_masked.nii.gz\")).get_fdata()\n",
+ "local_field_brain = nib.load(os.path.join(\"field_simulations\", \"brain\", \"local_field.nii.gz\")).get_fdata()\n",
+ "\n",
+ "fig = make_subplots(rows=1, cols=3, shared_xaxes=False, horizontal_spacing=0.13, vertical_spacing = 0.12, subplot_titles=(f\"Susceptibility distribution ({CHI_UNICODE})\", \"Simulated B0 map\", \"Simulated B0 map
no background field\"), specs=[[{\"type\": \"Heatmap\"}, {\"type\": \"Heatmap\"}, {\"type\": \"Heatmap\"}]])\n",
+ "fig.add_trace(go.Heatmap(z=susc, colorscale='gray', colorbar_x=1/3 - 0.09, colorbar=dict(title=\"ppm\", titleside=\"top\")), 1, 1)\n",
+ "fig.add_trace(go.Heatmap(z=fmap_hz_all, colorscale='gray', colorbar_x=2/3 - 0.045, colorbar=dict(title=\"Hz\", titleside=\"top\")), 1, 2)\n",
+ "fig.add_trace(go.Heatmap(z=local_field_cyl, colorscale='gray', colorbar_x=1-0.004, colorbar=dict(title=\"Hz\", titleside=\"top\")), 1, 3)\n",
+ "fig.add_trace(go.Heatmap(z=np.rot90(susc_brain, k=-1), colorscale='gray', colorbar_x=1/3 - 0.09, zmin=-0.5, zmax=0.5, colorbar=dict(title=\"ppm\", titleside=\"top\"), visible=False), 1, 1)\n",
+ "fig.add_trace(go.Heatmap(z=np.rot90(fmap_hz_brain_all, k=-1), colorscale='gray', colorbar_x=2/3 - 0.045, zmin=1100, zmax=2300, colorbar=dict(title=\"Hz\", titleside=\"top\"), visible=False), 1, 2)\n",
+ "fig.add_trace(go.Heatmap(z=np.rot90(local_field_brain, k=-1), colorscale='gray', zmin=-4, zmax=4, colorbar_x=1-0.004, colorbar=dict(title=\"Hz\", titleside=\"top\"), visible=False), 1, 3)\n",
+ "\n",
+ "### Create buttons for drop down menu\n",
+ "labels = [\"Cylinders\", \"Brain\"]\n",
+ "buttons = []\n",
+ "for i, label in enumerate(labels):\n",
+ " if label == \"Cylinders\":\n",
+ " visibility = [True, True, True, False, False, False]\n",
+ " else:\n",
+ " visibility = [False, False, False, True, True, True]\n",
+ " button = dict(\n",
+ " label = label,\n",
+ " method = 'update',\n",
+ " args = [{'visible': visibility},\n",
+ " {'title': label}])\n",
+ " buttons.append(button)\n",
+ "\n",
+ "updatemenus = list([\n",
+ " dict(active=0,\n",
+ " x=-0.15,\n",
+ " buttons=buttons,\n",
+ " showactive=True,\n",
+ " )\n",
+ "])\n",
+ "\n",
+ "fig.update_xaxes(showticklabels=False)\n",
+ "fig.update_yaxes(showticklabels=False)\n",
+ "fig.update_layout({\"height\": 380, \"width\": 900},\n",
+ " title_text=\"B0 maps from susceptibility simulations\",\n",
+ " title_x=0.5,\n",
+ " updatemenus=updatemenus,\n",
+ " showlegend=False\n",
+ " )\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1aaf54a3-7715-4379-9ff2-874bb6e2d6b2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Note: Field was reduced a lot to be able to show the sinusoid\n",
+ "# Note: *2 after lowpass filter is because this is a single coil (sin instead of e^(-ix)) and demodulating by multiplying a sinusoid creates a 1/2 difference. In practice, since we have both x and y components, we can recover the full signal instead of doing X2.\n",
+ "GYRO_BAR_RATIO_H = 42.6e6 # [Hz/T]\n",
+ "b0 = 0.000002 # [T]\n",
+ "T2 = 0.3 # s\n",
+ "y_0_cst = 100\n",
+ "fs = 10000\n",
+ "\n",
+ "f_larmor = b0 * GYRO_BAR_RATIO_H\n",
+ "t = np.linspace(0, 1, fs + 1) # 1 second\n",
+ "\n",
+ "def butter_lowpass(cutoff, fs, order=5):\n",
+ " return butter(order, cutoff, fs=fs, btype='low', analog=False)\n",
+ "\n",
+ "def butter_lowpass_filter(data, cutoff, fs, order=5):\n",
+ " b, a = butter_lowpass(cutoff, fs, order=order)\n",
+ " y = filtfilt(b, a, data, method='gust')\n",
+ " return y\n",
+ "\n",
+ "# Lab frame\n",
+ "y_0 = y_0_cst * np.sin(2 * math.pi * f_larmor * t)\n",
+ "exp = np.exp(-t/T2)\n",
+ "y = y_0 * exp / y_0_cst\n",
+ "temp = y * (y_0 / y_0_cst)\n",
+ "y_demod = butter_lowpass_filter(temp, f_larmor, fs, order=5) * 2\n",
+ "\n",
+ "fig = go.Figure()\n",
+ "fig.add_scatter(x=t, y=y, name=\"FID\")\n",
+ "fig.add_scatter(x=t, y=y_demod, name=\"Demodulated FID\")\n",
+ "fig.add_scatter(x=t, y=exp, name=\"T2 decay\")\n",
+ "\n",
+ "# 2 isochromats\n",
+ "y_1amp = y_0_cst / 10\n",
+ "y_1 = y_1amp * np.sin(2 * math.pi * (f_larmor + 10) * t)\n",
+ "y = (y_0 + y_1) * exp / (y_0_cst + y_1amp)\n",
+ "temp = y * (y_0 / y_0_cst)\n",
+ "y_demod = butter_lowpass_filter(temp, f_larmor, fs, order=5) * 2\n",
+ "\n",
+ "fig.add_scatter(x=t, y=y, name=\"FID\", visible=False)\n",
+ "fig.add_scatter(x=t, y=y_demod, name=\"Demodulated FID\", visible=False)\n",
+ "fig.add_scatter(x=t, y=exp, name=\"T2 decay\", visible=False)\n",
+ "\n",
+ "# Multiple isochromats\n",
+ "n_freqs = 100\n",
+ "fid = y_0 * exp\n",
+ "y_sum_demod = butter_lowpass_filter(fid * (y_0 / y_0_cst), f_larmor, fs, order=5) * 2\n",
+ "for i in range(n_freqs):\n",
+ " amp = 1\n",
+ " freq_offset = 10\n",
+ " scale = freq_offset/n_freqs\n",
+ " mid = n_freqs // 2\n",
+ " y_1 = amp * np.sin(2 * math.pi * (f_larmor + scale*(mid - i)) * t) * exp\n",
+ " fid += y_1\n",
+ " y_demod = butter_lowpass_filter(y_1 * (y_0 / y_0_cst), f_larmor, fs, order=5) * 2\n",
+ " y_sum_demod += y_demod\n",
+ "\n",
+ "y_demod_scaled = y_sum_demod / (y_0_cst + (n_freqs * amp))\n",
+ "fid_scaled = fid / (y_0_cst + (n_freqs * amp))\n",
+ "\n",
+ "fig.add_scatter(x=t, y=fid_scaled, name=\"FID\", visible=False)\n",
+ "fig.add_scatter(x=t, y=y_demod_scaled, name=\"Demodulated FID\", visible=False)\n",
+ "fig.add_scatter(x=t, y=exp, name=\"T2 decay\", visible=False)\n",
+ "fig.update_traces(marker=dict(size=3))\n",
+ "fig.update_layout(\n",
+ " title=\"Single species\",\n",
+ " title_x=0.5,\n",
+ " updatemenus=[\n",
+ " dict(\n",
+ " buttons=list([\n",
+ " dict(\n",
+ " args=[{\"visible\": [True, True, True, False, False, False, False, False, False]},\n",
+ " {\"title\": \"Single species\"}],\n",
+ " label=\"Single species\",\n",
+ " method=\"update\"\n",
+ " ),\n",
+ " dict(\n",
+ " args=[{\"visible\": [False, False, False, True, True, True, False, False, False]},\n",
+ " {\"title\": \"Two species\"}],\n",
+ " label=\"Two species\",\n",
+ " method=\"update\"\n",
+ " ),\n",
+ " dict(\n",
+ " args=[{\"visible\": [False, False, False, False, False, False, True, True, True]},\n",
+ " {\"title\": \"Multiple Species\"}],\n",
+ " label=\"Multiple Species\",\n",
+ " method=\"update\"\n",
+ " )\n",
+ " ]),\n",
+ " direction=\"down\",\n",
+ " showactive=True,\n",
+ "\n",
+ " ),\n",
+ " ])\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "720b531c-3095-4ded-beb4-ea3ec8ac78cd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def calc_dk(gx, gy, dt):\n",
+ " dkx = GYRO_BAR_RATIO_H * gx * dt\n",
+ " dky = GYRO_BAR_RATIO_H * gy * dt\n",
+ " return (dkx, dky)\n",
+ "\n",
+ "gy_bad = 100e-6\n",
+ "\n",
+ "end_time = 0.0912\n",
+ "n_times = 913\n",
+ "t = np.linspace(0, end_time, n_times)\n",
+ "dt = end_time / n_times\n",
+ "nx = 64\n",
+ "k = np.zeros([n_times, 2])\n",
+ "k_distorted = np.zeros([n_times, 2])\n",
+ "for it in range(1, n_times):\n",
+ " \n",
+ " if it <= 20:\n",
+ " gx = -40e-3\n",
+ " gy = -40e-3\n",
+ " else:\n",
+ " n_steps = 138\n",
+ " i = (it - 20) % n_steps\n",
+ " if i <= 0:\n",
+ " gx = 0\n",
+ " gy = 25e-3\n",
+ " elif i <= nx:\n",
+ " gx = 25e-3\n",
+ " gy = 0\n",
+ " elif i <= nx + 5:\n",
+ " gx = 0\n",
+ " gy = 25e-3\n",
+ " elif i <= (2*nx) + 5:\n",
+ " gx = -25e-3\n",
+ " gy = 0\n",
+ " elif i <= n_steps:\n",
+ " gx = 0\n",
+ " gy = 25e-3\n",
+ "\n",
+ " dkx, dky = calc_dk(gx, gy, dt)\n",
+ " kx = k[it - 1, 0] + dkx\n",
+ " ky = k[it - 1, 1] + dky\n",
+ " k[it, :] = [kx, ky]\n",
+ " dkx, dky = calc_dk(gx, gy + gy_bad, dt)\n",
+ " kx = k_distorted[it - 1, 0] + dkx\n",
+ " ky = k_distorted[it - 1, 1] + dky\n",
+ " k_distorted[it, :] = [kx, ky]\n",
+ "\n",
+ "\n",
+ "fig = go.Figure()\n",
+ "fig.add_trace(go.Scatter(x=k[:, 0], y=k[:, 1],\n",
+ " mode='lines',\n",
+ " line=dict(color='#636EFA'),\n",
+ " name='Theoretical trajectory'))\n",
+ "fig.add_trace(go.Scatter(x=k_distorted[:, 0], y=k_distorted[:, 1],\n",
+ " mode='lines',\n",
+ " line=dict(color='#fa6363'),\n",
+ " name='Inhomogeneous trajectory'))\n",
+ "frames = [dict(\n",
+ " data=[go.Scatter(x=k[:2*i, 0], y=k[:2*i, 1],\n",
+ " mode='lines',\n",
+ " line=dict(color='#636EFA'),\n",
+ " name='Theoretical trajectory'),\n",
+ " go.Scatter(x=k_distorted[:2*i, 0], y=k_distorted[:2*i, 1],\n",
+ " mode='lines',\n",
+ " line=dict(color='#fa6363'),\n",
+ " name='Inhomogeneous trajectory')],\n",
+ " name=str(i),\n",
+ " traces=[0,1]) for i in range(int(n_times/2))]\n",
+ "fig.frames = frames\n",
+ "\n",
+ "fig.update_xaxes(range=[-3500, 3500])\n",
+ "fig.update_yaxes(range=[-3700, 3700])\n",
+ "fig.update_xaxes(showticklabels=False)\n",
+ "fig.update_yaxes(showticklabels=False)\n",
+ "fig.update_layout(title=\"K-space Trajectory\",\n",
+ " title_x=0.5,\n",
+ " height=600,\n",
+ " width=700,\n",
+ " updatemenus=[dict(\n",
+ " type='buttons',\n",
+ " buttons=[dict(label='Play',\n",
+ " method='animate',\n",
+ " args=[None, dict(frame=dict(duration=15, redraw=False), transition=dict(duration=15), fromcurrent=True, mode='immediate')]),\n",
+ " dict(label='Pause',\n",
+ " method='animate',\n",
+ " args=[[None], dict(frame=dict(duration=0, redraw=False), mode='immediate')])\n",
+ " ])])\n",
+ "fig.show()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/05-B0/content/chapter4_2.ipynb b/05-B0/content/chapter4_2.ipynb
new file mode 100644
index 0000000..a67b3d0
--- /dev/null
+++ b/05-B0/content/chapter4_2.ipynb
@@ -0,0 +1,473 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "63c39eb3-2ac0-4456-a905-d6981285a9b6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import plotly.express as px\n",
+ "import os\n",
+ "import nibabel as nib\n",
+ "import numpy as np\n",
+ "from plotly.subplots import make_subplots\n",
+ "import plotly.graph_objects as go\n",
+ "import math\n",
+ "import json\n",
+ "PI_UNICODE = \"\\U0001D70B\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21fa46b1-3008-42fd-a05e-a09e0a5fb5be",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Parameters\n",
+ "num_frames = 200\n",
+ "omega_0 = 1 # Larmor frequency\n",
+ "omega_1 = 0.9 # Inhomogeneous spin\n",
+ "time_max = 5 # [s]\n",
+ "\n",
+ "# Initial phase of the spin\n",
+ "initial_phase = 0.5\n",
+ "\n",
+ "# Time array\n",
+ "time = np.linspace(0, time_max, num_frames)\n",
+ "\n",
+ "# Generate data for spins\n",
+ "x = np.cos(omega_0 * (2*math.pi) * time + initial_phase)\n",
+ "y = np.sin(omega_0 * (2*math.pi) * time + initial_phase)\n",
+ "x1 = np.cos(omega_1 * (2*math.pi) * time + initial_phase)\n",
+ "y1 = np.sin(omega_1 * (2*math.pi) * time + initial_phase)\n",
+ "\n",
+ "# Generate data for spins in rotating frame of reference\n",
+ "x_rot = np.cos((omega_0-omega_0) * (2*math.pi) * time + initial_phase)\n",
+ "y_rot = np.sin((omega_0-omega_0) * (2*math.pi) * time + initial_phase)\n",
+ "x1_rot = np.cos((omega_1-omega_0) * (2*math.pi) * time + initial_phase)\n",
+ "y1_rot = np.sin((omega_1-omega_0) * (2*math.pi) * time + initial_phase)\n",
+ "\n",
+ "# Calculate angles\n",
+ "angles = (np.arctan2(y,x))\n",
+ "angles1 = (np.arctan2(y1,x1))\n",
+ "angles_rot = (np.arctan2(y_rot,x_rot))\n",
+ "angles1_rot = (np.arctan2(y1_rot,x1_rot))\n",
+ "\n",
+ "# Create figure\n",
+ "fig = make_subplots(rows=1, cols=2, shared_xaxes=False, horizontal_spacing=0.1, subplot_titles=(\"Spin Rotating Through Time\", \"Phase Evolution of the Signal (rad)\"))\n",
+ "\n",
+ "# Add spin as an arrow\n",
+ "fig.add_trace(go.Scatter(\n",
+ " x=[0, x[0]],\n",
+ " y=[0, y[0]],\n",
+ " mode='lines+markers',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='blue', width=3),\n",
+ " name='Spin at f0'),\n",
+ " row=1, col=1)\n",
+ "fig.add_trace(go.Scatter(\n",
+ " x=[0, x1[0]],\n",
+ " y=[0, y1[0]],\n",
+ " mode='lines+markers',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='red', width=3),\n",
+ " name='Inhomogeneous Spin'),\n",
+ " row=1, col=1)\n",
+ "\n",
+ "# Add phase of the signal\n",
+ "fig.add_trace(go.Scatter(\n",
+ " x=[time[0]],\n",
+ " y=[angles[0]],\n",
+ " mode='markers',\n",
+ " marker=dict(color='blue', size=5),\n",
+ " name='Phase'),\n",
+ " row=1, col=2\n",
+ ")\n",
+ "fig.add_trace(go.Scatter(\n",
+ " x=[time],\n",
+ " y=[angles1],\n",
+ " mode='markers',\n",
+ " marker=dict(color='red', size=5),\n",
+ " name='Inhomogeneous Phase'),\n",
+ " row=1, col=2\n",
+ ")\n",
+ "# Rotating frame\n",
+ "fig.add_trace(go.Scatter(\n",
+ " x=[0, x_rot[0]],\n",
+ " y=[0, y_rot[0]],\n",
+ " mode='lines+markers',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='blue', width=3),\n",
+ " name='Spin at f0', visible=False),\n",
+ " row=1, col=1)\n",
+ "fig.add_trace(go.Scatter(\n",
+ " x=[0, x1_rot[0]],\n",
+ " y=[0, y1_rot[0]],\n",
+ " mode='lines+markers',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='red', width=3),\n",
+ " name='Inhomogeneous Spin', visible=False),\n",
+ " row=1, col=1)\n",
+ "\n",
+ "# Add phase of the signal\n",
+ "fig.add_trace(go.Scatter(\n",
+ " x=[time[0]],\n",
+ " y=[angles_rot[0]],\n",
+ " mode='markers',\n",
+ " marker=dict(color='blue', size=5),\n",
+ " name='Phase', visible=False),\n",
+ " row=1, col=2\n",
+ ")\n",
+ "fig.add_trace(go.Scatter(\n",
+ " x=[time],\n",
+ " y=[angles1_rot],\n",
+ " mode='markers',\n",
+ " marker=dict(color='red', size=5),\n",
+ " name='Inhomogeneous Phase', visible=False),\n",
+ " row=1, col=2\n",
+ ")\n",
+ "\n",
+ "fig.update_xaxes(range=[-1.1, 1.1], row=1, col=1)\n",
+ "fig.update_yaxes(range=[-1.1, 1.1], row=1, col=1)\n",
+ "fig.update_xaxes(range=[np.min(time), np.max(time)], row=1, col=2)\n",
+ "fig.update_yaxes(range=[np.min(angles) + 0.1*np.min(angles), np.max(angles) + 0.1*np.max(angles)], row=1, col=2)\n",
+ "\n",
+ "# Add frames\n",
+ "frames = [dict(\n",
+ " data=[go.Scatter(x=[0, x[i]],\n",
+ " y=[0, y[i]],\n",
+ " mode='lines+markers',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='#636EFA', width=3),\n",
+ " name='Spin at f0'),\n",
+ " go.Scatter(x=[0, x1[i]],\n",
+ " y=[0, y1[i]],\n",
+ " mode='lines+markers',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='#fa6363', width=3),\n",
+ " name='Inhomogeneous Spin'),\n",
+ " go.Scatter(x=time[:i],\n",
+ " y=angles[:i],\n",
+ " mode='lines',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='blue', width=3),\n",
+ " name='Phase'),\n",
+ " go.Scatter(x=time[:i],\n",
+ " y=angles1[:i],\n",
+ " mode='lines',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='red', width=3),\n",
+ " name='Inhomogeneous Phase'),\n",
+ " go.Scatter(x=[0, x_rot[i]],\n",
+ " y=[0, y_rot[i]],\n",
+ " mode='lines+markers',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='#636EFA', width=3),\n",
+ " name='Spin at f0'),\n",
+ " go.Scatter(x=[0, x1_rot[i]],\n",
+ " y=[0, y1_rot[i]],\n",
+ " mode='lines+markers',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='#fa6363', width=3),\n",
+ " name='Inhomogeneous Spin'),\n",
+ " go.Scatter(x=time[:i],\n",
+ " y=angles_rot[:i],\n",
+ " mode='lines',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='blue', width=3),\n",
+ " name='Phase'),\n",
+ " go.Scatter(x=time[:i],\n",
+ " y=angles1_rot[:i],\n",
+ " mode='lines',\n",
+ " marker=dict(size=5),\n",
+ " line=dict(color='red', width=3),\n",
+ " name='Inhomogeneous Phase')\n",
+ " \n",
+ " \n",
+ " ],\n",
+ " name=str(i),\n",
+ " traces=[0,1,2,3,4,5,6,7]) for i in range(num_frames)]\n",
+ "\n",
+ "fig.frames = frames\n",
+ "\n",
+ "# Determine the maximum absolute value of coordinates\n",
+ "max_coord = max(abs(x.max()), abs(y.max()))\n",
+ "\n",
+ "fig.update_xaxes(title_text=\"x\", row=1, col=1)\n",
+ "fig.update_xaxes(title_text=\"time\", row=1, col=2)\n",
+ "fig.update_yaxes(title_text=\"y\", row=1, col=1)\n",
+ "fig.update_yaxes(title_text=\"rad\", tickmode = 'array',\n",
+ " tickvals = [-math.pi, 0, math.pi],\n",
+ " ticktext = [f\"-{PI_UNICODE}\", 0, f'{PI_UNICODE}'], row=1, col=2)\n",
+ "\n",
+ "\n",
+ "# Update layout\n",
+ "fig.update_layout(\n",
+ " height=600,\n",
+ " title=\"Spins rotating\",\n",
+ " xaxis=dict(autorange=False),\n",
+ " yaxis=dict(autorange=False),\n",
+ " updatemenus=\n",
+ " [dict(\n",
+ " type='buttons',\n",
+ " buttons=[dict(label='Play',\n",
+ " method='animate',\n",
+ " args=[None, dict(frame=dict(duration=50, redraw=False), fromcurrent=True, mode='immediate')]),\n",
+ " dict(label='Pause',\n",
+ " method='animate',\n",
+ " args=[[None], dict(frame=dict(duration=0, redraw=False), mode='immediate')])\n",
+ " ],\n",
+ " ),\n",
+ " dict(\n",
+ " buttons=[dict(\n",
+ " args=[{\"visible\": [True, True, True, True, False, False, False, False]}],\n",
+ " label=\"Laboratory Frame\",\n",
+ " method=\"update\"),\n",
+ " dict(\n",
+ " args=[{\"visible\": [False, False, False, False, True, True, True, True]}],\n",
+ " label=\"Rotating Frame\",\n",
+ " method=\"update\"\n",
+ " )],\n",
+ " direction=\"down\",\n",
+ " pad={\"b\": 70},\n",
+ " showactive=True,\n",
+ " x=-0.13,\n",
+ " xanchor=\"left\",\n",
+ " y=-0.15,\n",
+ " yanchor=\"top\"\n",
+ " )\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "# Show figure\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "867c0c25-97f5-4236-95e3-17982e3e5be8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fname_mag_e1 = os.path.join(\"fmap\", \"sub-fmap_magnitude1.nii.gz\")\n",
+ "fname_phase_e1 = os.path.join(\"fmap\", \"sub-fmap_phase1.nii.gz\")\n",
+ "fname_phase_e1_json = os.path.join(\"fmap\", \"sub-fmap_phase1.json\")\n",
+ "fname_phase_e2 = os.path.join(\"fmap\", \"sub-fmap_phase2.nii.gz\")\n",
+ "fname_mask = os.path.join(\"fmap\", \"mask.nii.gz\")\n",
+ "fname_fmap = os.path.join(\"fmap\", \"fmap.nii.gz\")\n",
+ "\n",
+ "nii_mag_e1 = nib.load(fname_mag_e1)\n",
+ "nii_phase_e1 = nib.load(fname_phase_e1)\n",
+ "nii_phase_e2 = nib.load(fname_phase_e2)\n",
+ "nii_mask = nib.load(fname_mask)\n",
+ "nii_fmap = nib.load(fname_fmap)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3827145b-5911-480c-ab37-1acdb99bb918",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Phase evolution though different echo times\n",
+ "mask = nii_mask.get_fdata()[30:-30,8:105,30]\n",
+ "fmap = nii_fmap.get_fdata()[30:-30,8:105,30] * mask # [Hz]\n",
+ "phase1 = (nii_phase_e1.get_fdata()[30:-30,8:105,30] / 4095 * 2 * math.pi - math.pi) * mask\n",
+ "\n",
+ "with open(fname_phase_e1_json, 'r') as json_data1:\n",
+ " data1 = json.load(json_data1)\n",
+ " \n",
+ "echo_time1 = data1['EchoTime']\n",
+ "phase0 = phase1 - (echo_time1 * (fmap * 2 * math.pi))\n",
+ "zmin = -math.pi\n",
+ "zmax = math.pi\n",
+ "\n",
+ "steps = 31\n",
+ "last_echo_time = 0.03\n",
+ "echo_times = np.linspace(0.0, last_echo_time, steps)\n",
+ "fig = go.Figure()\n",
+ "for i_echo, echo_time in enumerate(echo_times):\n",
+ " phase = phase0 + (fmap * echo_time * 2 * math.pi)\n",
+ " phase = np.angle(np.exp(1j*phase))\n",
+ " if i_echo >= len(echo_times) - 1:\n",
+ " fig.add_trace(go.Heatmap(z=np.rot90(phase, k=-1), visible=True, coloraxis = \"coloraxis\"))\n",
+ " else:\n",
+ " fig.add_trace(go.Heatmap(z=np.rot90(phase, k=-1), visible=False, coloraxis = \"coloraxis\"))\n",
+ "\n",
+ "fig.update_layout(coloraxis = {'colorscale':'gray'},\n",
+ " coloraxis_cmin=zmin, coloraxis_cmax=zmax)\n",
+ "fig.update_coloraxes(\n",
+ " colorbar=dict(title=\"Rad\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\",\n",
+ " tickvals=[-math.pi, 0, math.pi-0.01],\n",
+ " ticktext = [f\"-{PI_UNICODE}\", 0, f'{PI_UNICODE}']))\n",
+ "\n",
+ "echo_times_str = [f\"{time:.2}\" for time in echo_times]\n",
+ "steps = []\n",
+ "for i in range(len(fig.data)):\n",
+ " step = dict(\n",
+ " method=\"update\",\n",
+ " label=echo_times_str[i],\n",
+ " args=[{\"visible\": [False] * len(fig.data)}], # layout attribute\n",
+ " )\n",
+ " step[\"args\"][0][\"visible\"][i] = True # Toggle i'th trace to \"visible\"\n",
+ " steps.append(step)\n",
+ "\n",
+ "sliders = [dict(\n",
+ " active=30,\n",
+ " currentvalue={\"prefix\": \"Echo Time: \"},\n",
+ " steps=steps\n",
+ ")]\n",
+ "\n",
+ "fig.update_layout(\n",
+ " sliders=sliders\n",
+ ")\n",
+ "\n",
+ "fig.update_layout(\n",
+ " title=dict(text=\"Phase at different echo times\", x=0.5)\n",
+ ")\n",
+ "fig.update_xaxes(showticklabels=False)\n",
+ "fig.update_yaxes(showticklabels=False)\n",
+ "fig.update_layout({\"height\": 550, \"width\": 500})\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5e51f4c7-1f99-4610-b610-c2eafe51d311",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def complex_difference(phase1, phase2):\n",
+ " \"\"\" Calculates the complex difference between 2 phase arrays (phase2 - phase1)\n",
+ "\n",
+ " Args:\n",
+ " phase1 (numpy.ndarray): Array containing phase data in radians\n",
+ " phase2 (numpy.ndarray): Array containing phase data in radians. Must be the same shape as phase1.\n",
+ "\n",
+ " Returns:\n",
+ " numpy.ndarray: The difference in phase between each voxels of phase2 and phase1 (phase2 - phase1)\n",
+ " \"\"\"\n",
+ "\n",
+ " # Calculate phasediff using complex difference\n",
+ " comp_0 = np.exp(-1j * phase1)\n",
+ " comp_1 = np.exp(1j * phase2)\n",
+ " return np.angle(comp_0 * comp_1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7d308a9a-1257-4535-b536-88646075b068",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def plot_2_echo_fmap(phase1, phase2, echotime1, echotime2):\n",
+ " phase_diff = complex_difference(phase1, phase2)\n",
+ " fmap = phase_diff / (echotime2 - echotime1) / 2 / math.pi\n",
+ " n=4\n",
+ " # Attempt at subplots\n",
+ " fig = make_subplots(rows=1, cols=n, shared_xaxes=False, horizontal_spacing=0.1, subplot_titles=(\"Phase 1\", \"Phase 2\", \"Phase difference\", \"B0 field map\"), specs=[[{\"type\": \"Heatmap\"}, {\"type\": \"Heatmap\"}, {\"type\": \"Heatmap\"}, {\"type\": \"Heatmap\"}]], )\n",
+ " \n",
+ " fig.add_trace(go.Heatmap(z=np.rot90(phase1, k=-1), colorscale='gray', colorbar_x=1/n - 0.05, zmin=-math.pi, zmax=math.pi,\n",
+ " colorbar=dict(title=\"Rad\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\",\n",
+ " tickvals=[-math.pi, 0, math.pi-0.01],\n",
+ " ticktext = [f\"-{PI_UNICODE}\", 0, f'{PI_UNICODE}'])), 1, 1)\n",
+ " fig.add_trace(go.Heatmap(z=np.rot90(phase2, k=-1), colorscale='gray', colorbar_x=2/n - 0.02, zmin=-math.pi, zmax=math.pi,\n",
+ " colorbar=dict(title=\"Rad\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\",\n",
+ " tickvals=[-math.pi, 0, math.pi-0.01],\n",
+ " ticktext = [f\"-{PI_UNICODE}\", 0, f'{PI_UNICODE}'])), 1, 2)\n",
+ " fig.add_trace(go.Heatmap(z=np.rot90(phase_diff, k=-1), colorscale='gray', colorbar_x=3/n - 0.02, zmin=-math.pi, zmax=math.pi,\n",
+ " colorbar=dict(title=\"Rad\",\n",
+ " titleside=\"top\",\n",
+ " tickmode=\"array\",\n",
+ " tickvals=[-math.pi, 0, math.pi-0.01],\n",
+ " ticktext = [f\"-{PI_UNICODE}\", 0, f'{PI_UNICODE}'])), 1, 3)\n",
+ " fig.add_trace(go.Heatmap(z=np.rot90(fmap, k=-1), colorscale='gray',\n",
+ " colorbar=dict(title=\"Hz\",\n",
+ " titleside=\"top\")), 1, 4)\n",
+ " fig.update_xaxes(showticklabels=False)\n",
+ " fig.update_yaxes(showticklabels=False)\n",
+ " fig.update_layout({\"height\": 450, \"width\": 1700})\n",
+ " \n",
+ " fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "909bbd00-9f7e-41f0-b59d-6ce94f02834c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_circle(x, y, r):\n",
+ " if x < 1 or y < 1 or r < 1:\n",
+ " raise ValueError(\"Input parameters are too small\")\n",
+ " \n",
+ " my_array = np.zeros([x,y])\n",
+ " for i in range(x):\n",
+ " for j in range(y):\n",
+ " squared = (i-(x/2))**2 + (j-(y/2))**2\n",
+ " h = np.sqrt(squared)\n",
+ " if h < r:\n",
+ " my_array[i,j] = 1\n",
+ " return my_array\n",
+ "\n",
+ "echo1 = get_circle(100, 100, 30) * -1\n",
+ "echo2 = get_circle(100, 100, 30) * 2\n",
+ "echo_time1 = 0.005\n",
+ "echo_time2 = 0.01\n",
+ "\n",
+ "plot_2_echo_fmap(echo1, echo2, echo_time1, echo_time2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "34fffc05-5890-48dc-b13f-3fa00a3e5c49",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "mask = nii_mask.get_fdata()[30:-30,8:105,30]\n",
+ "phase1 = (nii_phase_e1.get_fdata()[30:-30,8:105,30] / 4095 * 2 * math.pi - math.pi) * mask\n",
+ "phase2 = (nii_phase_e2.get_fdata()[30:-30,8:105,30] / 4095 * 2 * math.pi - math.pi) * mask\n",
+ "echo_time1 = 0.00338\n",
+ "echo_time2 = 0.00558\n",
+ "\n",
+ "plot_2_echo_fmap(phase1, phase2, echo_time1, echo_time2)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/05-B0/content/chapter4_3.ipynb b/05-B0/content/chapter4_3.ipynb
new file mode 100644
index 0000000..a1c6d5e
--- /dev/null
+++ b/05-B0/content/chapter4_3.ipynb
@@ -0,0 +1,291 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7ee2da8d-6336-4054-99c7-bc374f0a0e7b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import plotly.express as px\n",
+ "import os\n",
+ "import nibabel as nib\n",
+ "import numpy as np\n",
+ "from plotly.subplots import make_subplots\n",
+ "import plotly.graph_objects as go\n",
+ "import math\n",
+ "from sklearn.linear_model import LinearRegression\n",
+ "from dash import Dash, dcc, html\n",
+ "import pandas as pd\n",
+ "PI_UNICODE = \"\\U0001D70B\"\n",
+ "GYRO_BAR_RATIO_H = 42.6e6 # [Hz/T]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "35eff464-d7ad-4124-b892-a57b6c4d410a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Taken from real brain data\n",
+ "phase1, phase2, phase3, phase4 = (2.9298516102709202, -0.21864564255753116, -2.2884910587688285, 2.392827225041896)\n",
+ "beg = 0\n",
+ "end = 0.015\n",
+ "\n",
+ "t = np.array([0.00263, 0.00526, 0.009, 0.013])\n",
+ "n_echoes = len(t)\n",
+ "y = np.array([phase1, phase2, phase3, phase4])\n",
+ "y_unwrapped = np.array([phase1, phase2, phase3, phase4 - (2 * math.pi)])\n",
+ "\n",
+ "reg = LinearRegression().fit(t.reshape(-1, 1), y_unwrapped.reshape(-1,1))\n",
+ "# Slope of linear regression reshaped into the shape of original 3D phase.\n",
+ "fieldmap_rad = reg.coef_[0] # [rad / s]\n",
+ "fieldmap_intercept = reg.intercept_[0] # [rad / s]\n",
+ "\n",
+ "t_predict = np.array([beg, end])\n",
+ "y_predict = reg.predict(t_predict.reshape(-1,1))[:,0]\n",
+ "\n",
+ "\n",
+ "fig = go.Figure()\n",
+ "fig.add_trace(go.Scatter(x=t, y=y, mode='markers', marker=dict(color='blue'), name='Original Phase'))\n",
+ "fig.add_trace(go.Scatter(x=[t[3]], y=[phase4- (2*math.pi)], mode='markers', marker=dict(color='red'), name='Unwrapped Phase'))\n",
+ "fig.add_trace(go.Scatter(x=t_predict, y=y_predict, mode='lines', marker=dict(color='green'), name='Fit'))\n",
+ "fig.add_trace(go.Scatter(x=[beg, end], y=[math.pi, math.pi], mode='lines', line=dict(color='gray'), showlegend=False))\n",
+ "fig.add_trace(go.Scatter(x=[beg, end], y=[-math.pi, -math.pi], mode='lines', line=dict(color='gray'), showlegend=False))\n",
+ "\n",
+ "fig.update_xaxes(title_text=\"Time (ms)\", range=[beg, end])\n",
+ "fig.update_yaxes(title_text=\"Phase (rad)\", tickmode = 'array', range=[-7,7],\n",
+ " tickvals = [-2*math.pi, -math.pi, 0, math.pi, 2*math.pi],\n",
+ " ticktext = [f'-2{PI_UNICODE}', f'-{PI_UNICODE}', '0', f'{PI_UNICODE}', f'2{PI_UNICODE}'])\n",
+ "fig.update_layout({\"width\": 800})\n",
+ "\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12c5e585-9ce1-4e46-afea-6f5960f09d31",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t = np.linspace(0, 10, 1001)\n",
+ "a = 2\n",
+ "y = a * t - 10\n",
+ "\n",
+ "# \n",
+ "phase = np.angle(np.exp(1j*y))\n",
+ "\n",
+ "fig = go.Figure()\n",
+ "fig.add_trace(go.Scatter(x=t, y=phase, mode='lines', name='Wrapped phase'))\n",
+ "fig.add_scatter(x=t, y=y, mode='lines', name='True phase')\n",
+ "fig.update_traces(marker=dict(size=3))\n",
+ "fig.update_xaxes(title_text=\"x\")\n",
+ "fig.update_yaxes(title_text=\"rad\", tickmode = 'array',\n",
+ " tickvals = [-3*math.pi, -2*math.pi, -math.pi, 0, math.pi, 2*math.pi, 3*math.pi],\n",
+ " ticktext = [f'-3{PI_UNICODE}', f'-2{PI_UNICODE}', f'-{PI_UNICODE}', '0', f'{PI_UNICODE}', f'2{PI_UNICODE}', f'3{PI_UNICODE}'])\n",
+ "fig.update_layout({\"width\": 800})\n",
+ "fig.show()\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a3750f9e-0258-4118-810c-fbb96d376b4d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import dash\n",
+ "import dash_bootstrap_components as dbc\n",
+ "from dash import Dash, html, dcc\n",
+ "import plotly.express as px\n",
+ "\n",
+ "t = np.linspace(0, 3, 1001)\n",
+ "y_unwrapped = t + t**2 - 3 * t**3 + t**4\n",
+ "y_wrapped = np.mod(y_unwrapped, 2 * math.pi);\n",
+ "\n",
+ "y_delta = y_unwrapped.max() - y_unwrapped.min()\n",
+ "min_y = y_unwrapped.min() - 2 * math.pi - y_delta * 0.05\n",
+ "max_y = y_unwrapped.max() + 2 * math.pi + y_delta * 0.05\n",
+ "\n",
+ "fig1 = go.Figure()\n",
+ "fig1.add_trace(go.Scatter(x=t, y=y_wrapped,\n",
+ " mode='lines',\n",
+ " name='Wrapped '))\n",
+ "fig1.update_layout(title_text=\"Wrapped\", title_x=0.25, showlegend=True,\n",
+ " legend={\"x\": 0.03, \"y\": 0.95})\n",
+ "fig1.update_yaxes(range=[min_y, max_y], title_text=\"rad\", tickmode = 'array',\n",
+ " tickvals = [-2*math.pi, -math.pi, 0, math.pi, 2*math.pi, 3*math.pi, 4*math.pi, 5*math.pi],\n",
+ " ticktext = [f'-2{PI_UNICODE}', f'-{PI_UNICODE}', '0', f'{PI_UNICODE}', f'2{PI_UNICODE}', f'3{PI_UNICODE}', f'4{PI_UNICODE}', f'5{PI_UNICODE}'])\n",
+ "fig1.update_xaxes(fixedrange=True)\n",
+ "fig1.update_yaxes(fixedrange=True)\n",
+ "\n",
+ "fig2 = go.Figure()\n",
+ "fig2.add_trace(go.Scatter(x=t, y=y_unwrapped,\n",
+ " mode='lines',\n",
+ " name='Solution 1', showlegend=True))\n",
+ "fig2.add_trace(go.Scatter(x=t, y=y_unwrapped+2*math.pi,\n",
+ " mode='lines',\n",
+ " name='Solution 2', showlegend=True))\n",
+ "fig2.add_trace(go.Scatter(x=t, y=y_unwrapped-2*math.pi,\n",
+ " mode='lines',\n",
+ " name='Solution 3', showlegend=True))\n",
+ "fig2.update_layout(title_text=\"Unwrapped\", title_x=0.75,\n",
+ " legend={\"x\": 0.85, \"y\": 0.95})\n",
+ "fig2.update_yaxes(range=[min_y, max_y], title_text=\"rad\", tickmode = 'array',\n",
+ " tickvals = [-2*math.pi, -math.pi, 0, math.pi, 2*math.pi, 3*math.pi, 4*math.pi, 5*math.pi],\n",
+ " ticktext = [f'-2{PI_UNICODE}', f'-{PI_UNICODE}', '0', f'{PI_UNICODE}', f'2{PI_UNICODE}', f'3{PI_UNICODE}', f'4{PI_UNICODE}', f'5{PI_UNICODE}'])\n",
+ "fig2.update_xaxes(fixedrange=True)\n",
+ "fig2.update_yaxes(fixedrange=True)\n",
+ "app = Dash(\n",
+ " __name__,\n",
+ " external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME],\n",
+ " external_scripts=[{'src':\"https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js\"}]\n",
+ ")\n",
+ "\n",
+ "def beforeAfterSlide(fig1, fig2, style=None):\n",
+ " bfA = []\n",
+ " if not style:\n",
+ " style = {'width':'100vw', 'height':'100vh'}\n",
+ " for key in style:\n",
+ " if '%' in style[key]:\n",
+ " if key in ['width', 'left']:\n",
+ " style[key] = style[key].replace('%','vw')\n",
+ " if key in ['top', 'height']:\n",
+ " style[key] = style[key].replace('%','vh')\n",
+ " bfA.append(html.Div(dcc.Graph(figure=fig2, style=style), className='after'))\n",
+ " bfA.append(html.Div(className='middle'))\n",
+ " bfA.append(html.Div(dcc.Graph(figure=fig1, style=style), className='before'))\n",
+ " return html.Div(bfA, className='beforeAfter', style=style)\n",
+ "\n",
+ "app.layout = html.Div(beforeAfterSlide(fig1, fig2, {'height':'75%', 'width':'75%', 'top':'10%', 'left':'6%'}))\n",
+ "\n",
+ "if __name__ == \"__main__\":\n",
+ " app.run_server(debug=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6e5ff4f8-d6aa-46f0-beba-ac9e58e225ed",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "seed = 2\n",
+ "np.random.seed(seed)\n",
+ "n_points = 20\n",
+ "len_x = 500\n",
+ "len_y = 500\n",
+ "map = np.zeros([len_x, len_y]) - math.pi\n",
+ "x, y = np.meshgrid(range(len_x), range(len_y))\n",
+ "\n",
+ "def gauss(a, std_x, std_y, center):\n",
+ " x0 = int(center[0] * len_x)\n",
+ " y0 = int(center[1] * len_y)\n",
+ " return a * np.exp(-((((x-x0)**2)/(std_x**2)) + (((y-y0)**2)/(std_y**2))))\n",
+ "\n",
+ "\n",
+ "for i_point in range(n_points):\n",
+ " point = (np.random.uniform(), np.random.uniform())\n",
+ " amp = np.random.randint(4, 9)\n",
+ " std_x = np.random.randint(50, 100)\n",
+ " std_y = np.random.randint(50, 100)\n",
+ " map += gauss(amp, std_x, std_y, point)\n",
+ "\n",
+ "wrapped = np.angle(np.exp(1j*map))\n",
+ "\n",
+ "fig = make_subplots(rows=1, cols=2, shared_xaxes=False, horizontal_spacing=0.1, subplot_titles=(\"Wrapped\", \"Unwrapped\"), specs=[[{\"type\": \"Heatmap\"}, {\"type\": \"Heatmap\"}]])\n",
+ "fig.add_trace(go.Heatmap(z=wrapped, colorscale='gray', colorbar_x=0.47, colorbar=dict(title=\"Rad\", titleside=\"top\", tickmode=\"array\", tickvals=[-math.pi, 0, math.pi], ticktext = [f'-{PI_UNICODE}', 0, f'{PI_UNICODE}'])), 1, 1)\n",
+ "fig.add_trace(go.Heatmap(z=map, colorscale='gray', colorbar=dict(title=\"Rad\", titleside=\"top\", tickmode=\"array\",\n",
+ " tickvals=[-2*math.pi, 0, 2*math.pi, 4*math.pi, 6*math.pi, 8*math.pi, 10*math.pi, 12*math.pi, 14*math.pi],\n",
+ " ticktext = [f'-2{PI_UNICODE}', 0, f'2{PI_UNICODE}', f'4{PI_UNICODE}', f'6{PI_UNICODE}', f'8{PI_UNICODE}', f'10{PI_UNICODE}', f'12{PI_UNICODE}', f'14{PI_UNICODE}'])), 1, 2)\n",
+ "fig.update_layout({\"height\": 500, \"width\": 1000})\n",
+ "fig.update_xaxes(showticklabels=False)\n",
+ "fig.update_yaxes(showticklabels=False)\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "277d6bb9-e3b3-448d-bc50-99f834fb83c9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "len_x = 500\n",
+ "len_y = 500\n",
+ "x, y = np.meshgrid(np.linspace(-len_x, len_x/2, len_x//2), np.linspace(-len_y, len_y, len_y), indexing='ij')\n",
+ "x1 = np.array(x)\n",
+ "y1 = np.array(y)\n",
+ "x, y = np.meshgrid(np.linspace(-len_x/2, len_x, len_x//2), np.linspace(-len_y, len_y, len_y), indexing='ij')\n",
+ "x2 = np.array(x)\n",
+ "y2 = np.array(y)\n",
+ "x = np.zeros([len_x, len_y])\n",
+ "y = np.zeros([len_x, len_y])\n",
+ "x[:len_x//2] = x1\n",
+ "y[:len_x//2,:] = -y1\n",
+ "x[len_x//2:,:] = -x2\n",
+ "y[len_x//2:,:] = -y2\n",
+ "\n",
+ "\n",
+ "phase = np.rot90(np.arctan2(y.astype(float),x.astype(float)),k=-1)\n",
+ "fig = make_subplots(rows=1, cols=1, subplot_titles=[(\"Phase Singularities\")], specs=[[{\"type\": \"Heatmap\"}]])\n",
+ "fig.add_trace(go.Heatmap(z=phase, colorscale='gray', colorbar=dict(title=\"Rad\", titleside=\"top\", tickmode=\"array\", tickvals=[-math.pi+0.01, 0, math.pi-0.01], ticktext = [f'-{PI_UNICODE}', 0, f'{PI_UNICODE}'])))\n",
+ "fig.add_trace(go.Scatter(x=[370, 370], y=[90, 410], mode='markers', marker=dict(size=10), line=dict(color='red')))\n",
+ "\n",
+ "fig.add_shape(type=\"line\", x0=370, y0=90, x1=370, y1=410, line=dict(color=\"red\",width=3))\n",
+ "fig.add_shape(type=\"path\", path=\"M 370,90 Q 200,250 370,410\", line=dict(color=\"red\",width=3))\n",
+ "fig.add_annotation(x=390, y=90, text=\"A\", showarrow=False)\n",
+ "fig.add_annotation(x=390, y=410, text=\"B\", showarrow=False, font=dict(color=\"white\"))\n",
+ "fig.update_layout({\"width\": 500, \"height\": 400})\n",
+ "fig.update_xaxes(showticklabels=False)\n",
+ "fig.update_yaxes(showticklabels=False)\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "edb89952-5341-4c4a-b8ca-6f5844a18141",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "b0 = np.array((0.064, 0.1, 0.3, 1.5, 3, 7)) # [T]\n",
+ "f0 = b0 * GYRO_BAR_RATIO_H # [Hz]\n",
+ "df = 1e-6 * f0\n",
+ "te = 1e3/(2*df)\n",
+ "\n",
+ "tmp = {\n",
+ " 'B0': list(b0),\n",
+ " 'TE (ms)': list(te)\n",
+ "}\n",
+ "df = pd.DataFrame(tmp)\n",
+ "# df.to_markdown()\n",
+ "df = df.T.style.hide(axis='columns')"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/05-B0/content/chapter4_4.ipynb b/05-B0/content/chapter4_4.ipynb
new file mode 100644
index 0000000..ce4f18c
--- /dev/null
+++ b/05-B0/content/chapter4_4.ipynb
@@ -0,0 +1,404 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "925fbe02-42e5-4449-9411-6c21926305ca",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import plotly.express as px\n",
+ "import os\n",
+ "import nibabel as nib\n",
+ "import numpy as np\n",
+ "from plotly.subplots import make_subplots\n",
+ "import plotly.graph_objects as go\n",
+ "import math\n",
+ "from sklearn.linear_model import LinearRegression\n",
+ "import copy\n",
+ "PI_UNICODE = \"\\U0001D70B\"\n",
+ "DELTA_UNICODE = \"\\u0394\"\n",
+ "GYRO_UNICODE = \"\\U0001D6FE\"\n",
+ "GREEK_DELTA_UNICODE = \"\\u03B4\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e0f59d72-c91a-454d-b99b-7032358665e5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "phase1, phase2, phase3 = (-4, -2, 0)\n",
+ "beg = 0\n",
+ "end = 0.01\n",
+ "t = np.array([0.00263, 0.00526, 0.009])\n",
+ "n_echoes = len(t)\n",
+ "y1 = np.array([phase1, phase2, phase3])\n",
+ "y2 = y1 + 2 * math.pi\n",
+ "\n",
+ "# Linear fit of the first line\n",
+ "reg1 = LinearRegression().fit(t.reshape(-1, 1), y1.reshape(-1,1))\n",
+ "fieldmap_rad1 = reg1.coef_[0] # [rad / s]\n",
+ "fieldmap_intercept1 = reg1.intercept_[0] # [rad / s]\n",
+ "t_predict1 = np.array([beg, end])\n",
+ "y_predict1 = reg1.predict(t_predict1.reshape(-1,1))[:,0]\n",
+ "\n",
+ "# Linear fit of the second line\n",
+ "reg2 = LinearRegression().fit(t.reshape(-1, 1), y2.reshape(-1,1))\n",
+ "fieldmap_rad2 = reg2.coef_[0] # [rad / s]\n",
+ "fieldmap_intercept2 = reg2.intercept_[0] # [rad / s]\n",
+ "t_predict2 = np.array([beg, end])\n",
+ "y_predict2 = reg2.predict(t_predict2.reshape(-1,1))[:,0]\n",
+ "\n",
+ "# Plot\n",
+ "fig = go.Figure()\n",
+ "fig.add_trace(go.Scatter(x=t, y=y1, mode='markers', marker=dict(color='blue'), name='Unwrapped solution 1'))\n",
+ "fig.add_trace(go.Scatter(x=t_predict1, y=y_predict1, mode='lines', marker=dict(color='blue'), name='Fit'))\n",
+ "fig.add_trace(go.Scatter(x=t, y=y2, mode='markers', marker=dict(color='red'), name='Unwrapped solution 2'))\n",
+ "fig.add_trace(go.Scatter(x=t_predict2, y=y_predict2, mode='lines', marker=dict(color='red'), name='Fit'))\n",
+ "fig.add_annotation(x=0.004, y=-3, text=f\"Slope: {GYRO_UNICODE}{DELTA_UNICODE}B*t\")\n",
+ "fig.add_annotation(x=0.004, y=3.3, text=f\"Slope: {GYRO_UNICODE}{DELTA_UNICODE}B*t\")\n",
+ "fig.update_xaxes(title_text=\"Time (ms)\", range=[beg, end])\n",
+ "fig.update_yaxes(title_text=\"Phase (rad)\", tickmode = 'array', range=[-8,8],\n",
+ " tickvals = [-2*math.pi, -math.pi, 0, math.pi, 2*math.pi],\n",
+ " ticktext = [f'-2{PI_UNICODE}', f'-{PI_UNICODE}', '0', f'{PI_UNICODE}', f'2{PI_UNICODE}'])\n",
+ "fig.update_layout({\"width\": 800})\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "09b870d2-8be9-4c7c-8e26-64292afc7874",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "phase1, phase2, phase3 = (-1, 2, 14)\n",
+ "phase_wrapped1 = 14-4*math.pi\n",
+ "phase_unwrapped1 = 14-2*math.pi\n",
+ "beg = 0\n",
+ "end = 0.016\n",
+ "\n",
+ "t1 = np.array([0.00263, 0.00526])\n",
+ "t2 = np.array([0.00263, 0.00526, 0.015])\n",
+ "n_echoes1 = len(t1)\n",
+ "n_echoes2 = len(t2)\n",
+ "y1 = np.array([phase1, phase2])\n",
+ "y2 = np.array([phase1, phase2, phase3])\n",
+ "\n",
+ "reg1 = LinearRegression().fit(t1.reshape(-1, 1), y1.reshape(-1,1))\n",
+ "# Slope of linear regression reshaped into the shape of original 3D phase.\n",
+ "fieldmap_rad1 = reg1.coef_[0] # [rad / s]\n",
+ "fieldmap_intercept1 = reg1.intercept_[0] # [rad / s]\n",
+ "t_predict1 = np.array([beg, end])\n",
+ "y_predict1 = reg1.predict(t_predict1.reshape(-1,1))[:,0]\n",
+ "\n",
+ "reg2 = LinearRegression().fit(t2.reshape(-1, 1), y2.reshape(-1,1))\n",
+ "# Slope of linear regression reshaped into the shape of original 3D phase.\n",
+ "fieldmap_rad2 = reg2.coef_[0] # [rad / s]\n",
+ "fieldmap_intercept2 = reg2.intercept_[0] # [rad / s]\n",
+ "t_predict2 = np.array([beg, end])\n",
+ "y_predict2 = reg2.predict(t_predict2.reshape(-1,1))[:,0]\n",
+ "\n",
+ "fig = go.Figure()\n",
+ "fig.add_trace(go.Scatter(x=t2, y=y2, mode='markers', marker=dict(color='green'), name='Unwrapped phase'))\n",
+ "fig.add_trace(go.Scatter(x=[t2[2]], y=[phase_wrapped1], mode='markers', marker=dict(color='blue'), name='Acquired wrapped phase'))\n",
+ "fig.add_trace(go.Scatter(x=[t2[2]], y=[phase_unwrapped1], mode='markers', marker=dict(color='orange'), name='Wrong unwrapped phase'))\n",
+ "fig.add_trace(go.Scatter(x=t_predict1, y=y_predict1, mode='lines', marker=dict(color='blue'), name='Fit with first 2 echoes'))\n",
+ "fig.add_trace(go.Scatter(x=t_predict2, y=y_predict2, mode='lines', marker=dict(color='green'), name='Fit with 3 echoes'))\n",
+ "fig.add_trace(go.Scatter(x=[beg, end], y=[math.pi, math.pi], mode='lines', line=dict(color='gray'), showlegend=False))\n",
+ "fig.add_trace(go.Scatter(x=[beg, end], y=[-math.pi, -math.pi], mode='lines', line=dict(color='gray'), showlegend=False))\n",
+ "fig.update_xaxes(title_text=\"Time (ms)\", range=[beg, end])\n",
+ "fig.update_yaxes(title_text=\"Phase (rad)\", tickmode = 'array', range=[-5,15],\n",
+ " tickvals = [-2*math.pi, -math.pi, 0, math.pi, 2*math.pi, 3*math.pi, 4*math.pi, 5*math.pi],\n",
+ " ticktext = [f'-2{PI_UNICODE}', f'-{PI_UNICODE}', '0', f'{PI_UNICODE}', f'2{PI_UNICODE}', f'3{PI_UNICODE}', f'4{PI_UNICODE}', f'5{PI_UNICODE}'])\n",
+ "fig.update_layout({\"width\": 800}, title_text=f\"Using fast {DELTA_UNICODE}TE to help temporally unwrapped 3rd echo\", title_x=0.5)\n",
+ "\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6a3b1dbd-4e48-4d61-b528-ca612768cafc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def complex_difference(phase1, phase2):\n",
+ " \"\"\" Calculates the complex difference between 2 phase arrays (phase2 - phase1)\n",
+ "\n",
+ " Args:\n",
+ " phase1 (numpy.ndarray): Array containing phase data in radians\n",
+ " phase2 (numpy.ndarray): Array containing phase data in radians. Must be the same shape as phase1.\n",
+ "\n",
+ " Returns:\n",
+ " numpy.ndarray: The difference in phase between each voxels of phase2 and phase1 (phase2 - phase1)\n",
+ " \"\"\"\n",
+ "\n",
+ " # Calculate phasediff using complex difference\n",
+ " comp_0 = np.exp(-1j * phase1)\n",
+ " comp_1 = np.exp(1j * phase2)\n",
+ " return np.angle(comp_0 * comp_1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e138a1ad-3945-4f6e-a51c-dd13c9bf6309",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def umpire_3echoes(phases, times):\n",
+ " \"\"\"\n",
+ " This function performs unwrapping using the UMPIRE algorithm with 3 echoes. UMPIRE requires echo times that are unevenly spaced.\n",
+ " \"\"\"\n",
+ " \n",
+ " # Complex difference\n",
+ " dpTE2 = complex_difference(phases[1], phases[2])\n",
+ " dpTE1 = complex_difference(phases[0], phases[1])\n",
+ " dpd = complex_difference(dpTE1, dpTE2)\n",
+ " # print(\"Diff in phase diff:\" , dpd)\n",
+ " dTEs = np.array([times[1]-times[0], times[2]-times[1]])\n",
+ " dt_dpd = dTEs[1] - dTEs[0]\n",
+ " \n",
+ " # Slope\n",
+ " slope = dpd / dt_dpd\n",
+ " \n",
+ " # n wraps in differences\n",
+ " n_wraps_dp = np.round((dTEs - dTEs*slope) / (2*math.pi))\n",
+ " \n",
+ " # Remove wraps in differences\n",
+ " dpTE1_prime = dpTE1 - (2*n_wraps_dp[0]*math.pi)\n",
+ " dpTE2_prime = dpTE2 - (2*n_wraps_dp[1]*math.pi)\n",
+ " \n",
+ " # Calculate better slope\n",
+ " slope_prime1 = dpTE1_prime / dTEs[0]\n",
+ " slope_prime2 = dpTE2_prime / dTEs[1]\n",
+ " slope_avg = (slope_prime1 + slope_prime2) / 2\n",
+ " \n",
+ " # Calculate wraps in original phase\n",
+ " n_wraps = np.round((phases - t*slope_avg) / (2*math.pi))\n",
+ " \n",
+ " # Remove wraps\n",
+ " unwrapped_with_phase_offset = phases - 2*math.pi*n_wraps\n",
+ " \n",
+ " # # Calculate receiver offset\n",
+ " # r = (t[0] * unwrapped_with_phase_offset[1] - t[1] * unwrapped_with_phase_offset[0]) / dTEs[0]\n",
+ "\n",
+ " # # Remove receiver phase offset\n",
+ " # phase_no_offset = complex_difference(r, unwrapped_with_phase_offset)\n",
+ " # # Unwrap one last time\n",
+ " # ns = np.round((phase_no_offset - t*slope_avg) / (2*math.pi))\n",
+ " # unwrapped_umpire = phase_no_offset - 2*math.pi*ns\n",
+ " \n",
+ " return unwrapped_with_phase_offset"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "23f65935-9811-445a-a2b7-f00fca06ed02",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t = np.array([0.003, 0.011, 0.020])\n",
+ "y_unwrapped = np.array([1.0, 9.05, 17.75])\n",
+ "wrapped = copy.deepcopy(y_unwrapped)\n",
+ "wrapped[0] = np.angle(np.exp(1j*wrapped[0]))\n",
+ "wrapped[1] = np.angle(np.exp(1j*wrapped[1]))\n",
+ "wrapped[2] = np.angle(np.exp(1j*wrapped[2]))\n",
+ "beg = 0.0\n",
+ "end = 0.021\n",
+ "\n",
+ "# Fit original data\n",
+ "reg1 = LinearRegression().fit(t.reshape(-1, 1), y_unwrapped.reshape(-1,1))\n",
+ "fieldmap_rad1 = reg1.coef_[0] # [rad / s]\n",
+ "fieldmap_intercept1 = reg1.intercept_[0] # [rad / s]\n",
+ "t_predict1 = np.array([beg, end])\n",
+ "y_predict1 = reg1.predict(t_predict1.reshape(-1,1))[:,0]\n",
+ "\n",
+ "# Unwrap with UMPIRE\n",
+ "unwrapped_umpire = umpire_3echoes(wrapped, t)\n",
+ "\n",
+ "# Fit unwrapped data of UMPIRE\n",
+ "reg2 = LinearRegression().fit(t.reshape(-1, 1), unwrapped_umpire.reshape(-1,1))\n",
+ "# Slope of linear regression reshaped into the shape of original 3D phase.\n",
+ "fieldmap_rad2 = reg2.coef_[0] # [rad / s]\n",
+ "fieldmap_intercept2 = reg2.intercept_[0] # [rad / s]\n",
+ "t_predict2 = np.array([beg, end])\n",
+ "y_predict2 = reg2.predict(t_predict2.reshape(-1,1))[:,0]\n",
+ "\n",
+ "# Plot\n",
+ "height_annotations = 7*math.pi + 0.5\n",
+ "fig = go.Figure()\n",
+ "fig.add_trace(go.Scatter(x=t, y=wrapped, mode='markers', marker=dict(color='blue'), name='Wrapped'))\n",
+ "fig.add_trace(go.Scatter(x=t, y=y_unwrapped, mode='markers', marker=dict(color='red'), name='True phase'))\n",
+ "fig.add_trace(go.Scatter(x=t, y=unwrapped_umpire, mode='markers', marker=dict(color='green'), name='Umpire', visible='legendonly'))\n",
+ "fig.add_trace(go.Scatter(x=t_predict2, y=y_predict2, mode='lines', marker=dict(color='green'), name='Umpire fit', visible='legendonly'))\n",
+ "fig.add_trace(go.Scatter(x=[beg, end], y=[math.pi, math.pi], mode='lines', line=dict(color='gray'), showlegend=False))\n",
+ "fig.add_trace(go.Scatter(x=[beg, end], y=[-math.pi, -math.pi], mode='lines', line=dict(color='gray'), showlegend=False))\n",
+ "fig.add_trace(go.Scatter(x=[t[0], t[1]], y=[height_annotations, height_annotations], mode='lines+markers', \n",
+ " marker=dict(symbol=\"line-ns-open\", color=\"black\",size=10),\n",
+ " line=dict(color='black'), showlegend=False))\n",
+ "fig.add_trace(go.Scatter(x=[t[1], t[2]], y=[height_annotations, height_annotations], mode='lines+markers', \n",
+ " marker=dict(symbol=\"line-ns-open\", color=\"black\",size=10),\n",
+ " line=dict(color='black'), showlegend=False))\n",
+ "fig.add_trace(go.Scatter(x=[t[1], t[2]], y=[height_annotations-3, height_annotations-3], mode='lines+markers', \n",
+ " marker=dict(symbol=\"line-ns-open\", color=\"black\",size=10),\n",
+ " line=dict(color='black'), showlegend=False))\n",
+ "fig.add_trace(go.Scatter(x=[0.002, 0.002], y=[y_unwrapped[0], y_unwrapped[1]], mode='lines+markers', \n",
+ " marker=dict(symbol=\"line-ew-open\", color=\"black\",size=10),\n",
+ " line=dict(color='black'), showlegend=False))\n",
+ "fig.add_annotation(x=(t[1]-t[0])/2 + t[0], y=height_annotations+1.15, text=f\"{DELTA_UNICODE}TE1\", showarrow=False)\n",
+ "fig.add_annotation(x=(t[2]-t[1])/2 + t[1], y=height_annotations+1.15, text=f\"{DELTA_UNICODE}TE2\", showarrow=False)\n",
+ "fig.add_annotation(x=(t[2]-t[1])/2 + t[1], y=height_annotations+1.15-3, text=f\"{DELTA_UNICODE}TE1 + {GREEK_DELTA_UNICODE}TE\", showarrow=False)\n",
+ "fig.add_annotation(x=0.0015, y=(y_unwrapped[1] + y_unwrapped[0]) / 2, text=f\">2{PI_UNICODE}\", showarrow=False)\n",
+ "fig.update_xaxes(title_text=\"Time (ms)\", range=[beg, end])\n",
+ "fig.update_yaxes(title_text=\"Phase (rad)\", tickmode = 'array', range=[-4,25],\n",
+ " tickvals = [-2*math.pi, 0, 2*math.pi, 4*math.pi, 6*math.pi],\n",
+ " ticktext = [f'-2{PI_UNICODE}', '0', f'2{PI_UNICODE}', f'4{PI_UNICODE}', f'6{PI_UNICODE}'])\n",
+ "fig.update_layout({\"width\": 800}, title_text=\"Phase unwrapping using UMPIRE algorithm\", title_x=0.5)\n",
+ "fig.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "122984e1-72ea-43f1-af05-d3a3f965179d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t = np.array([0.003, 0.011, 0.020])\n",
+ "y_unwrapped = np.array([1.0, 9.05, 17.75])\n",
+ "wrapped = copy.deepcopy(y_unwrapped)\n",
+ "wrapped[0] = np.angle(np.exp(1j*wrapped[0]))\n",
+ "wrapped[1] = np.angle(np.exp(1j*wrapped[1]))\n",
+ "wrapped[2] = np.angle(np.exp(1j*wrapped[2]))\n",
+ "beg = 0.0\n",
+ "end = 0.021\n",
+ "\n",
+ "fig = go.Figure()\n",
+ "noises = np.arange(-0.5, 0.51, 0.01)\n",
+ "# Add traces, one for each slider step\n",
+ "for noise in noises:\n",
+ " # Noisy unwrapped data\n",
+ " unwrapped_noisy = copy.deepcopy(y_unwrapped)\n",
+ " unwrapped_noisy[1] += noise\n",
+ " fig.add_trace(\n",
+ " go.Scatter(\n",
+ " visible=False,\n",
+ " mode='markers',\n",
+ " marker=dict(color='red'),\n",
+ " name=\"True Phase\",\n",
+ " x=t,\n",
+ " y=unwrapped_noisy))\n",
+ "\n",
+ " # Fit of noisy unwrapped data\n",
+ " reg1 = LinearRegression().fit(t.reshape(-1, 1), unwrapped_noisy.reshape(-1,1))\n",
+ " fieldmap_rad1 = reg1.coef_[0] # [rad / s]\n",
+ " fieldmap_intercept1 = reg1.intercept_[0] # [rad / s]\n",
+ " t_predict1 = np.array([beg, end])\n",
+ " y_predict1 = reg1.predict(t_predict1.reshape(-1,1))[:,0]\n",
+ " fig.add_trace(go.Scatter(visible=False, x=t_predict1, y=y_predict1, mode='lines', marker=dict(color='red'), name='True linear fit'))\n",
+ "\n",
+ " # Noisy wrapped data\n",
+ " wrapped_noisy = copy.deepcopy(unwrapped_noisy)\n",
+ " wrapped_noisy[0] = np.angle(np.exp(1j*wrapped_noisy[0]))\n",
+ " wrapped_noisy[1] = np.angle(np.exp(1j*wrapped_noisy[1]))\n",
+ " wrapped_noisy[2] = np.angle(np.exp(1j*wrapped_noisy[2]))\n",
+ " fig.add_trace(\n",
+ " go.Scatter(\n",
+ " visible=False,\n",
+ " mode='markers',\n",
+ " marker=dict(color='blue'),\n",
+ " name=\"Wrapped Phase\",\n",
+ " x=t,\n",
+ " y=wrapped_noisy))\n",
+ "\n",
+ " # UMPIRE\n",
+ " unwrapped_umpire = umpire_3echoes(wrapped_noisy, t)\n",
+ " fig.add_trace(\n",
+ " go.Scatter(\n",
+ " visible=False,\n",
+ " x=t, y=unwrapped_umpire, mode='markers', marker=dict(color='green'), name='Umpire'))\n",
+ "\n",
+ " # Fit unwrapped data of UMPIRE\n",
+ " reg2 = LinearRegression().fit(t.reshape(-1, 1), unwrapped_umpire.reshape(-1,1))\n",
+ " # Slope of linear regression reshaped into the shape of original 3D phase.\n",
+ " fieldmap_rad2 = reg2.coef_[0] # [rad / s]\n",
+ " fieldmap_intercept2 = reg2.intercept_[0] # [rad / s]\n",
+ " t_predict2 = np.array([beg, end])\n",
+ " y_predict2 = reg2.predict(t_predict2.reshape(-1,1))[:,0]\n",
+ " \n",
+ " fig.add_trace(go.Scatter(visible=False, x=t_predict2, y=y_predict2, mode='lines', marker=dict(color='green'), name='Umpire fit'))\n",
+ " \n",
+ "active = len(noises)//2\n",
+ "fig.data[active].visible = True\n",
+ "fig.data[active+1].visible = True\n",
+ "fig.data[active+2].visible = True\n",
+ "fig.data[active+3].visible = True\n",
+ "fig.data[active+4].visible = True\n",
+ "\n",
+ "# Static plot\n",
+ "fig.add_trace(go.Scatter(x=[beg, end], y=[math.pi, math.pi], mode='lines', line=dict(color='gray'), showlegend=False))\n",
+ "fig.add_trace(go.Scatter(x=[beg, end], y=[-math.pi, -math.pi], mode='lines', line=dict(color='gray'), showlegend=False))\n",
+ "fig.update_xaxes(title_text=\"Time (ms)\", range=[beg, end])\n",
+ "fig.update_yaxes(title_text=\"Phase (rad)\", tickmode = 'array', range=[-4,25],\n",
+ " tickvals = [-2*math.pi, 0, 2*math.pi, 4*math.pi, 6*math.pi],\n",
+ " ticktext = [f'-2{PI_UNICODE}', '0', f'2{PI_UNICODE}', f'4{PI_UNICODE}', f'6{PI_UNICODE}'])\n",
+ "\n",
+ "# Create and add slider\n",
+ "phase_offsets = [f\"{i:.2}\" for i in noises]\n",
+ "steps = []\n",
+ "for i in range(len(noises)):\n",
+ " step = dict(\n",
+ " method=\"update\",\n",
+ " label=phase_offsets[i],\n",
+ " args=[{\"visible\": [False] * 5*len(noises) + [True] * (len(fig.data) - 5*len(noises))}], # layout attribute\n",
+ " )\n",
+ " step[\"args\"][0][\"visible\"][5*i] = True\n",
+ " step[\"args\"][0][\"visible\"][5*i + 1] = True\n",
+ " step[\"args\"][0][\"visible\"][5*i + 2] = True\n",
+ " step[\"args\"][0][\"visible\"][5*i + 3] = True\n",
+ " step[\"args\"][0][\"visible\"][5*i + 4] = True\n",
+ " steps.append(step)\n",
+ "\n",
+ "sliders = [dict(\n",
+ " active=active,\n",
+ " currentvalue={\"prefix\": \"2nd echo phase offset (rad): \"},\n",
+ " pad={\"t\": 50},\n",
+ " steps=steps\n",
+ ")]\n",
+ "\n",
+ "fig.update_layout({\"width\": 800},\n",
+ " sliders=sliders,\n",
+ " title_text=\"Effect of noise using UMPIRE phase unwrapping\", title_x=0.5)\n",
+ "\n",
+ "fig.show()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/05-B0/content/data.zip b/05-B0/content/data.zip
new file mode 100644
index 0000000..990ffe2
Binary files /dev/null and b/05-B0/content/data.zip differ
diff --git a/05-B0/content/figure.html b/05-B0/content/figure.html
new file mode 100644
index 0000000..7b237e8
--- /dev/null
+++ b/05-B0/content/figure.html
@@ -0,0 +1,14 @@
+
+