# Prepare Python environment
import numpy as np
import plotly.express as px
import os
import nibabel as nib
import numpy as np
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import math
from sklearn.linear_model import LinearRegression
import copy
PI_UNICODE = "\U0001D70B"
DELTA_UNICODE = "\u0394"
GYRO_UNICODE = "\U0001D6FE"
GREEK_DELTA_UNICODE = "\u03B4"
phase1, phase2, phase3 = (-1, 2, 14)
phase_wrapped1 = 14-4*math.pi
phase_unwrapped1 = 14-2*math.pi
beg = 0
end = 0.016
def complex_difference(phase1, phase2):
""" Calculates the complex difference between 2 phase arrays (phase2 - phase1)
Args:
phase1 (numpy.ndarray): Array containing phase data in radians
phase2 (numpy.ndarray): Array containing phase data in radians. Must be the same shape as phase1.
Returns:
numpy.ndarray: The difference in phase between each voxels of phase2 and phase1 (phase2 - phase1)
"""
# Calculate phasediff using complex difference
comp_0 = np.exp(-1j * phase1)
comp_1 = np.exp(1j * phase2)
return np.angle(comp_0 * comp_1)
def umpire_3echoes(phases, times):
"""
This function performs unwrapping using the UMPIRE algorithm with 3 echoes. UMPIRE requires echo times that are unevenly spaced.
"""
# Complex difference
dpTE2 = complex_difference(phases[1], phases[2])
dpTE1 = complex_difference(phases[0], phases[1])
dpd = complex_difference(dpTE1, dpTE2)
# print("Diff in phase diff:" , dpd)
dTEs = np.array([times[1]-times[0], times[2]-times[1]])
dt_dpd = dTEs[1] - dTEs[0]
# Slope
slope = dpd / dt_dpd
# n wraps in differences
n_wraps_dp = np.round((dTEs - dTEs*slope) / (2*math.pi))
# Remove wraps in differences
dpTE1_prime = dpTE1 - (2*n_wraps_dp[0]*math.pi)
dpTE2_prime = dpTE2 - (2*n_wraps_dp[1]*math.pi)
# Calculate better slope
slope_prime1 = dpTE1_prime / dTEs[0]
slope_prime2 = dpTE2_prime / dTEs[1]
slope_avg = (slope_prime1 + slope_prime2) / 2
# Calculate wraps in original phase
n_wraps = np.round((phases - t*slope_avg) / (2*math.pi))
# Remove wraps
unwrapped_with_phase_offset = phases - 2*math.pi*n_wraps
# # Calculate receiver offset
# r = (t[0] * unwrapped_with_phase_offset[1] - t[1] * unwrapped_with_phase_offset[0]) / dTEs[0]
# # Remove receiver phase offset
# phase_no_offset = complex_difference(r, unwrapped_with_phase_offset)
# # Unwrap one last time
# ns = np.round((phase_no_offset - t*slope_avg) / (2*math.pi))
# unwrapped_umpire = phase_no_offset - 2*math.pi*ns
return unwrapped_with_phase_offset
t = np.array([0.003, 0.011, 0.020])
y_unwrapped = np.array([1.0, 9.05, 17.75])
wrapped = copy.deepcopy(y_unwrapped)
wrapped[0] = np.angle(np.exp(1j*wrapped[0]))
wrapped[1] = np.angle(np.exp(1j*wrapped[1]))
wrapped[2] = np.angle(np.exp(1j*wrapped[2]))
beg = 0.0
end = 0.021
# Fit original data
reg1 = LinearRegression().fit(t.reshape(-1, 1), y_unwrapped.reshape(-1,1))
fieldmap_rad1 = reg1.coef_[0] # [rad / s]
fieldmap_intercept1 = reg1.intercept_[0] # [rad / s]
t_predict1 = np.array([beg, end])
y_predict1 = reg1.predict(t_predict1.reshape(-1,1))[:,0]
# Unwrap with UMPIRE
unwrapped_umpire = umpire_3echoes(wrapped, t)
# Fit unwrapped data of UMPIRE
reg2 = LinearRegression().fit(t.reshape(-1, 1), unwrapped_umpire.reshape(-1,1))
# Slope of linear regression reshaped into the shape of original 3D phase.
fieldmap_rad2 = reg2.coef_[0] # [rad / s]
fieldmap_intercept2 = reg2.intercept_[0] # [rad / s]
t_predict2 = np.array([beg, end])
y_predict2 = reg2.predict(t_predict2.reshape(-1,1))[:,0]
t = np.array([0.003, 0.011, 0.020])
y_unwrapped = np.array([1.0, 9.05, 17.75])
wrapped = copy.deepcopy(y_unwrapped)
wrapped[0] = np.angle(np.exp(1j*wrapped[0]))
wrapped[1] = np.angle(np.exp(1j*wrapped[1]))
wrapped[2] = np.angle(np.exp(1j*wrapped[2]))
beg = 0.0
end = 0.021
fig = go.Figure()
noises = np.arange(-0.5, 0.51, 0.01)
# Add traces, one for each slider step
for noise in noises:
# Noisy unwrapped data
unwrapped_noisy = copy.deepcopy(y_unwrapped)
unwrapped_noisy[1] += noise
fig.add_trace(
go.Scatter(
visible=False,
mode='markers',
marker=dict(color='red'),
name="True Phase",
x=t,
y=unwrapped_noisy))
# Fit of noisy unwrapped data
reg1 = LinearRegression().fit(t.reshape(-1, 1), unwrapped_noisy.reshape(-1,1))
fieldmap_rad1 = reg1.coef_[0] # [rad / s]
fieldmap_intercept1 = reg1.intercept_[0] # [rad / s]
t_predict1 = np.array([beg, end])
y_predict1 = reg1.predict(t_predict1.reshape(-1,1))[:,0]
fig.add_trace(go.Scatter(visible=False, x=t_predict1, y=y_predict1, mode='lines', marker=dict(color='red'), name='True linear fit'))
# Noisy wrapped data
wrapped_noisy = copy.deepcopy(unwrapped_noisy)
wrapped_noisy[0] = np.angle(np.exp(1j*wrapped_noisy[0]))
wrapped_noisy[1] = np.angle(np.exp(1j*wrapped_noisy[1]))
wrapped_noisy[2] = np.angle(np.exp(1j*wrapped_noisy[2]))
fig.add_trace(
go.Scatter(
visible=False,
mode='markers',
marker=dict(color='blue'),
name="Wrapped Phase",
x=t,
y=wrapped_noisy))
# UMPIRE
unwrapped_umpire = umpire_3echoes(wrapped_noisy, t)
fig.add_trace(
go.Scatter(
visible=False,
x=t, y=unwrapped_umpire, mode='markers', marker=dict(color='green'), name='Umpire'))
# Fit unwrapped data of UMPIRE
reg2 = LinearRegression().fit(t.reshape(-1, 1), unwrapped_umpire.reshape(-1,1))
# Slope of linear regression reshaped into the shape of original 3D phase.
fieldmap_rad2 = reg2.coef_[0] # [rad / s]
fieldmap_intercept2 = reg2.intercept_[0] # [rad / s]
t_predict2 = np.array([beg, end])
y_predict2 = reg2.predict(t_predict2.reshape(-1,1))[:,0]
fig.add_trace(go.Scatter(visible=False, x=t_predict2, y=y_predict2, mode='lines', marker=dict(color='green'), name='Umpire fit'))
active = len(noises)//2
fig.data[active].visible = True
fig.data[active+1].visible = True
fig.data[active+2].visible = True
fig.data[active+3].visible = True
fig.data[active+4].visible = True
# Static plot
fig.add_trace(go.Scatter(x=[beg, end], y=[math.pi, math.pi], mode='lines', line=dict(color='gray'), showlegend=False))
fig.add_trace(go.Scatter(x=[beg, end], y=[-math.pi, -math.pi], mode='lines', line=dict(color='gray'), showlegend=False))
fig.update_xaxes(title_text="Time (ms)", range=[beg, end])
fig.update_yaxes(title_text="Phase (rad)", tickmode = 'array', range=[-4,25],
tickvals = [-2*math.pi, 0, 2*math.pi, 4*math.pi, 6*math.pi],
ticktext = [f'-2{PI_UNICODE}', '0', f'2{PI_UNICODE}', f'4{PI_UNICODE}', f'6{PI_UNICODE}'])
# Create and add slider
phase_offsets = [f"{i:.2}" for i in noises]
steps = []
for i in range(len(noises)):
step = dict(
method="update",
label=phase_offsets[i],
args=[{"visible": [False] * 5*len(noises) + [True] * (len(fig.data) - 5*len(noises))}], # layout attribute
)
step["args"][0]["visible"][5*i] = True
step["args"][0]["visible"][5*i + 1] = True
step["args"][0]["visible"][5*i + 2] = True
step["args"][0]["visible"][5*i + 3] = True
step["args"][0]["visible"][5*i + 4] = True
steps.append(step)
sliders = [dict(
active=active,
currentvalue={"prefix": "2nd echo phase offset (rad): "},
pad={"t": 50},
steps=steps
)]
fig.update_layout({"width": 800},
sliders=sliders,
title_text="Effect of noise using UMPIRE phase unwrapping", title_x=0.5)
fig.show()
Loading...