def test_cross_point(self): from mr_utils.sim.ssfp import ssfp, get_cross_point, get_complex_cross_point # Get four phase cycled images I1 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=0) I2 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=np.pi / 2) I3 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=np.pi) I4 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=3 * np.pi / 2) # Find cross points x0, y0 = get_cross_point(I1, I2, I3, I4) M = get_complex_cross_point(I1, I2, I3, I4) # Make sure we get the same answer self.assertTrue(np.allclose(x0 + 1j * y0, M))
def test_many_phase_cycles_single_point(self): '''Make sure we can do a bunch of them at once.''' pcs = np.linspace(0, 2 * np.pi, 16, endpoint=False) Itrue = np.zeros(pcs.size, dtype='complex') for ii, pc in np.ndenumerate(pcs): Itrue[ii] = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, pc) Is = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, pcs) self.assertTrue(np.allclose(Itrue, Is))
def test_two_phase_cycles_single_point(self): '''Try doing two phase-cycles.''' # Gold standard is computing them individually pcs = np.linspace(0, 2 * np.pi, 2, endpoint=False) I0 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, pcs[0]) I1 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, pcs[1]) Itrue = np.stack((I0, I1)) # Now try all at once Is = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, pcs) self.assertTrue(np.allclose(Itrue, Is))
def test_simulated_field_maps(self): '''Simulate the field maps and see if we can recover them.''' # Get quantitative MR maps T1 = 1. T2 = .8 PD = 1 TR = 6e-3 alpha = np.deg2rad(10) phase_cyc = 0 # Monte Carlo: find the response that matches Mxy most closely num_sims = 1000 err = 0 tol = 1. beta = 1 # don't simulate df on the boundaries of the profile... dfs = np.arange(-1 / TR, 1 / TR, .1) # Do for all possible df for _ii in trange(num_sims, leave=False, desc='Monte Carlo'): # Measure the signal in the real off-resonance environment df_true = np.random.uniform(low=-1 / TR + beta, high=1 / TR - beta) Mxy = ssfp(T1, T2, TR, alpha, df_true, phase_cyc=phase_cyc, M0=PD) # Find closest match and take that to be the off-resonance value df0 = quantitative_fm(Mxy, dfs, T1, T2, PD, TR, alpha, phase_cyc) if np.abs(df_true - df0) > tol: print('True: %g, found: %g' % (df_true, df0)) err += 1 self.assertFalse(err)
def ssfp_dictionary_for_loop(T1s, T2s, TR, alphas, df): '''Verification for ssfp_dictionary generation. Parameters ========== T1s : array_like (1D) all T1 decay constant values to simulate. T2s : array_like (1D) all T2 decay constant values to simulate. TR : float repetition time for bSSFP simulation. alphas : array_like (1D) all flip angle values to simulate. df : array_like (1D) off-resonance frequencies over which to simulate. Returns ======= D : array_like Dictionary of simulated values keys : array_like Keys of dictionary D, all (T1, T2, alpha) combinations. ''' # Get keys from supplied params keys = get_keys(T1s, T2s, alphas) # Generate dictionary iterating over keys N = keys.shape[1] D = np.zeros((N, df.size), dtype='complex') for ii in range(N): for jj in range(df.size): D[ii, jj] = ssfp(keys[0, ii], keys[1, ii], TR, keys[2, ii], df[jj]) return (D, keys)
def test_banding_sim_2d(self): '''Make sure banding looks the same coming from NMR params and ESM.''' # To get periodic banding like we want to see, we need some serious # field inhomogeneity. dim = 256 min_df, max_df = 0, 500 x = np.linspace(min_df, max_df, dim) y = np.zeros(dim) field_map, _ = np.meshgrid(x, y) # # Show the field map # plt.imshow(field_map) # plt.show() # # Generate simulated banding image explicitly using NMR parameters sig0 = ssfp(self.T1, self.T2, self.TR, self.alpha, field_map) # plt.subplot(2,1,1) # plt.imshow(np.abs(sig0)) # plt.subplot(2,1,2) # plt.plot(np.abs(sig0[int(dim/2),:])) # plt.show() # Generate simulated banding image using elliptical signal model M, a, b = elliptical_params(self.T1, self.T2, self.TR, self.alpha) sig1 = ssfp_from_ellipse(M, a, b, self.TR, field_map) self.assertTrue(np.allclose(sig0, sig1))
def get_df_responses(T1, T2, PD, TR, alpha, phase_cyc, dfs): '''Simulate bSSFP response across all possible off-resonances. Parameters ========== T1 : float scalar T1 longitudinal recovery value in seconds. T2 : float scalar T2 transverse decay value in seconds. PD : float scalar proton density value scaled the same as acquisiton. TR : float Repetition time in seconds. alpha : float Flip angle in radians. phase_cyc : float RF phase cycling in radians. dfs : float Off-resonance values to simulate over. Returns ======= resp : array_like Frequency response of SSFP signal across entire spectrum. ''' # Feed ssfp sim an array of parameters to be used with all the df values T1s = np.ones(dfs.shape) * T1 T2s = np.ones(dfs.shape) * T2 PDs = np.ones(dfs.shape) * PD resp = ssfp(T1s, T2s, TR, alpha, dfs, phase_cyc=phase_cyc, M0=PDs) # Returns a vector of simulated Mxy with index corresponding to dfs return resp
def setUp(self): from mr_utils.sim.ssfp import ssfp self.TR = 6e-3 self.T1, self.T2 = 1, .8 self.alpha = np.pi / 3 # To get periodic banding like we want to see, we need some serious # field inhomogeneity. dim = 256 min_df, max_df = 0, 500 x = np.linspace(min_df, max_df, dim) y = np.zeros(dim) self.field_map, _ = np.meshgrid(x, y) # Get four phase cycled images self.I1 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.field_map, phase_cyc=0) self.I2 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.field_map, phase_cyc=np.pi / 2) self.I3 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.field_map, phase_cyc=np.pi) self.I4 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.field_map, phase_cyc=3 * np.pi / 2) self.Is = ssfp(self.T1, self.T2, self.TR, self.alpha, self.field_map, phase_cyc=[0, np.pi / 2, np.pi, 3 * np.pi / 2])
def bssfp_2d_cylinder(TR=6e-3, alpha=np.pi / 3, dims=(64, 64), FOV=((-1, 1), (-1, 1)), radius=.5, field_map=None, phase_cyc=0, kspace=False): '''Simulates axial bSSFP scan of cylindrical phantom. Parameters ---------- TR : float, optional Repetition time. alpha : float, optional Flip angle (in rad). dims : tuple of ints, optional Matrix size, (dim_x,dim_y) FOV : tuple of tuples, optional Field of view in arbitrary units: ( (x_min,x_max), (y_min,y_max) ) radius : float, optional Radius of cylinder in arbitrary units. field_map : array_like, optional (dim_x,dim_y) field map. If None, linear gradient in x used. phase_cyc : float, optional Phase cycling used in simulated bSSFP acquisition (in rad). kspace : bool, optional Whether or not to return data in kspace or imspace. Returns ------- im : array_like Complex simulated image with bSSFP contrast. ''' # Get the base cylinder maps PD, T1s, T2s = cylinder_2d(dims=dims, FOV=FOV, radius=radius) if field_map is None: min_df, max_df = 0, 500 fx = np.linspace(min_df, max_df, dims[0]) fy = np.zeros(dims[1]) field_map, _ = np.meshgrid(fx, fy) im = ssfp(T1s, T2s, TR, alpha, field_map, phase_cyc=phase_cyc, M0=PD).T # im = gre_sim(T1s,T2s,TR,TR/2,alpha,field_map,phi=phase_cyc, # dphi=phase_cyc,M0=PD,spoil=False,iter=200) # im[np.isnan(im)] = 0 # view(im) if kspace: return np.fft.fftshift(np.fft.fft2(np.fft.fftshift(im), axes=(0, 1)), axes=(0, 1)) #else... return im
def test_gre_unspoiled_and_bssfp(self): '''Very balanced steady state solution.''' M0, T1, T2 = 5.0, 1.0, .5 im1 = gre_sim(T1, T2, TR=self.TR, TE=self.TR/2, alpha=self.alpha, field_map=np.zeros(1), M0=M0, spoil=False, maxiter=200) im2 = ssfp(T1, T2, TR=self.TR, alpha=self.alpha, field_map=np.zeros(1), phase_cyc=0, M0=M0) # Currently failing... self.assertEqual(im1, im2)
def test_find_atom(self): from mr_utils.sim.ssfp import ssfp, ssfp_dictionary, find_atom D, keys = ssfp_dictionary(self.T1s, self.T2s, self.TR, self.alphas, self.df) sig = ssfp(self.T10, self.T20, self.TR, self.alpha0, self.df) found_params = find_atom(sig, D, keys) actual_params = np.array([self.T10, self.T20, self.alpha0]) self.assertTrue(np.allclose(actual_params, found_params))
def test_gs(self): from mr_utils.sim.ssfp import ssfp from mr_utils.recon.ssfp import gs_recon pcs = np.zeros((4, self.dim, self.dim), dtype='complex') for ii, pc in enumerate([0, np.pi / 2, np.pi, 3 * np.pi / 2]): pcs[ii, ...] = ssfp(**self.args, phase_cyc=pc) # view(pcs) recon = gs_recon(*[x.squeeze() for x in np.split(pcs, 4)])
def test_find_atom(self): '''Test method that finds atom in a given dictionary.''' D, keys = ssfp_dictionary(self.T1s, self.T2s, self.TR, self.alphas, self.df) sig = ssfp(self.T10, self.T20, self.TR, self.alpha0, self.df) found_params = find_atom(sig, D, keys) actual_params = np.array([self.T10, self.T20, self.alpha0]) self.assertTrue(np.allclose(actual_params, found_params))
def test_ssfp_sim(self): '''Generate signal from bSSFP signal eq and elliptical model.''' # Do it the "normal" way I0 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df) # Now do it using the elliptical model M, a, b = elliptical_params(self.T1, self.T2, self.TR, self.alpha) I1 = ssfp_from_ellipse(M, a, b, self.TR, self.df) self.assertTrue(np.allclose(I0, I1))
def test_ssfp_sim(self): from mr_utils.sim.ssfp import ssfp, get_theta, elliptical_params, ssfp_from_ellipse # Do it the "normal" way I0 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df) # Now do it using the elliptical model M, a, b = elliptical_params(self.T1, self.T2, self.TR, self.alpha) I1 = ssfp_from_ellipse(M, a, b, self.TR, self.df) self.assertTrue(np.allclose(I0, I1))
def test_t1_t2_field_map_mats(self): from mr_utils.sim.ssfp import ssfp # Let T1,T2, and field_map all be matrices over the entire 2d image sig = ssfp(self.T1s, self.T2s, self.TR, self.alpha, self.field_map, phase_cyc=0, M0=self.PD)
def plotEllipse(T1, T2, TR, alpha, offres, M0, dphi): '''Compute (x, y) coordinates for ellipse described by MR parameters. Parameters ========== T1 : float Longitudinal relaxation constant (in sec). T2 : float Transverse relaxation constant (in sec). TR : float Repetition time (in sec). alpha : float Flip angle (in rad). offres : float Off-resonance (in Hz). M0 : float Proton density. dphi : float or bool Phase-cycle value (in rad). dphi=True means use fixed linspace for dphi set, else, use the list of dphis provided. Returns ======= x : array_like x coordinate values of ellipse. y : array_like y coordinate values of ellipse. ''' if isinstance(dphi, bool): dphis = np.arange(0, 2 * np.pi, .01) Mxy = ssfp(T1, T2, TR, alpha, offres, phase_cyc=dphis, M0=M0) else: Mxy = ssfp(T1, T2, TR, alpha, offres, phase_cyc=dphis, M0=M0) x = Mxy.real y = Mxy.imag return (x, y)
def bssfp_acq(T1s, T2s, PD, field_map, TR=5e-3, alpha=np.deg2rad(10), phase_cyc=0): return (ssfp(T1s, T2s, TR, alpha, field_map=field_map, phase_cyc=phase_cyc, M0=PD))
def test_two_phase_cycles_multiple_point(self): '''Now make MxN param maps and simulate multiple phase-cycles.''' M, N = 10, 5 T1s = np.ones((M, N)) * self.T1 T2s = np.ones((M, N)) * self.T2 alphas = np.ones((M, N)) * self.alpha # Linear gradient for field map min_df, max_df = 0, 200 fx = np.linspace(min_df, max_df, N) fy = np.zeros(M) df, _ = np.meshgrid(fx, fy) # Gold standard is again, computing individually pcs = np.linspace(0, 2 * np.pi, 2, endpoint=False) I0 = ssfp(T1s, T2s, self.TR, alphas, df, pcs[0]) I1 = ssfp(T1s, T2s, self.TR, alphas, df, pcs[1]) Itrue = np.stack((I0, I1)) # Now try doing all at once # reps = (pcs.size, 1, 1) # pcs = np.tile(pcs, T1s.shape[:] + (1,)).transpose((2, 0, 1)) # T1s = np.tile(T1s, reps) # T2s = np.tile(T2s, reps) # alphas = np.tile(alphas, reps) # df = np.tile(df, reps) # print(T1s.shape, T2s.shape, alphas.shape, df.shape, pcs.shape) Is = ssfp(T1s, T2s, self.TR, alphas, df, pcs) # from mr_utils import view # view(np.vstack((Itrue, Is))) # print(Itrue.shape, Is.shape) self.assertTrue(np.allclose(Itrue, Is))
def setUp(self): self.num_pc = 6 self.pcs = [2 * np.pi * n / self.num_pc for n in range(self.num_pc)] self.TR = 10e-3 self.alpha = np.deg2rad(30) self.df = 1 / (5 * self.TR) self.T1 = 1.5 self.T2 = .8 self.T1s = np.linspace(.2, 2, 100) self.I = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=self.pcs)
def bssfp_acq(T1s, T2s, PD, field_map, TR=5e-3, alpha=np.deg2rad(10), phase_cyc=0): '''Wrapper to simulate bSSFP acquisition.''' return ssfp(T1s, T2s, TR, alpha, field_map=field_map, phase_cyc=phase_cyc, M0=PD)
def test_cross_point(self): '''Find cross point from cartesian and ESM function.''' # Get four phase cycled images I1 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=0) I2 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=np.pi / 2) I3 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=np.pi) I4 = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=3 * np.pi / 2) Is = ssfp(self.T1, self.T2, self.TR, self.alpha, self.df, phase_cyc=[0, np.pi / 2, np.pi, 3 * np.pi / 2]) # Find cross points x0, y0 = get_cross_point(I1, I2, I3, I4) M = get_complex_cross_point(Is) # Make sure we get the same answer self.assertTrue(np.allclose(x0 + 1j * y0, M))
def ssfp_dictionary(T1s, T2s, TR, alphas, df): '''Generate a dicionary of bSSFP profiles given parameters. Parameters ========== T1s : array_like (1D) all T1 decay constant values to simulate. T2s : array_like (1D) all T2 decay constant values to simulate. TR : float repetition time for bSSFP simulation. alphas : array_like (1D) all flip angle values to simulate. df : array_like (1D) off-resonance frequencies over which to simulate. Returns ======= D : array_like Dictionary of simulated values keys : array_like Keys of dictionary D, all (T1, T2, alpha) combinations. Notes ===== T1s,T2s,alphas should all be 1D arrays. All feasible combinations will be simulated (i.e., where T1 >= T2). The dictionary and keys are returned. Each dictionary column is the simulation over frequencies df. The keys are a list of tuples: (T1,T2,alpha). ''' # Get keys from supplied params keys = get_keys(T1s, T2s, alphas) # # Use more efficient matrix formulation # D = ssfp_old(keys[0, :], keys[1, :], TR, keys[2, :], df) # Right now we have to do it for every alpha because ssfp() can't handle # more than one alpha at a time... D = np.zeros((keys.shape[1], df.size), dtype='complex') for ii, alpha in np.ndenumerate(keys[2, :]): D[ii, :] = ssfp(keys[0, ii], keys[1, ii], TR, alpha, df) # D = np.zeros((keys.shape[1], df.size), dtype='complex') # for ii, alpha in np.ndenumerate(keys[2, :]): # for jj, df0 in np.ndenumerate(df): # D[ii, jj] = ssfp(keys[0, ii], keys[1, ii], TR, alpha, df0) return (D, keys)
def get_df_responses(T1, T2, PD, TR, alpha, phase_cyc, dfs): '''Simulate bSSFP response across all possible off-resonances. T1 -- scalar T1 longitudinal recovery value in seconds. T2 -- scalar T2 transverse decay value in seconds. PD -- scalar proton density value scaled the same as acquisiton. TR -- Repetition time in seconds. alpha -- Flip angle in radians. phase_cyc -- RF phase cycling in radians. dfs -- Off-resonance values to simulate over. ''' # Feed ssfp sim an array of parameters to be used with all the df values T1s = np.ones(dfs.shape) * T1 T2s = np.ones(dfs.shape) * T2 PDs = np.ones(dfs.shape) * PD resp = ssfp(T1s, T2s, TR, alpha, dfs, phase_cyc=phase_cyc, M0=PDs) # Returns a vector of simulated Mxy with index corresponding to dfs return (resp)
def runner(enum, T1, T2, TR, df, pcs, M0): '''Run PLANET fitting for the current iteration.''' idx = enum[0] nom_alpha, dev_alpha = enum[1][:] I = ssfp(T1, T2, TR, nom_alpha * (1 + dev_alpha), df, pcs, M0) try: _Meff, T10, T20 = PLANET(I, nom_alpha, TR, T1, disp=False) T1s = T10 T2s = T20 except AssertionError as _e: # tqdm.write(str(e)) T1s = np.nan T2s = np.nan except ValueError as _e: # tqdm.write(str(e)) T1s = np.nan T2s = np.nan except TypeError as _e: # tqdm.write(str(e)) T1s = np.nan T2s = np.nan return (idx, T1s, T2s)
def ellipticalfit(Ireal, TR, dphis, offres, M0, alpha, T1, T2): '''ELLIPTICALFIT Parameters ========== Ireal : array_like Hermtian transposed phase-cycle values for single pixel. TR : float Repetition time (in sec). M0 : Estmated proton density from the band reduction algorithm. phasecycles : Phase-cycles (in rad). offres : Off-resonance estimation (in Hz). Returns ======= J : array_like Real part of difference concatenated with imaginary part of difference ''' I = ssfp(T1, T2, TR, alpha, offres, phase_cyc=dphis, M0=M0) return (I - Ireal).view(dtype=c_double)
if __name__ == '__main__': # SSFP experiment params TR = 12e-3 alpha = np.deg2rad(10) lpcs = 64 pcs = np.linspace(-2 * np.pi, 2 * np.pi, lpcs, endpoint=False) # Experiment conditions and tissue params T1 = .830 T2 = .080 M0 = 1 dfs = np.linspace(-1 / TR, 1 / TR, lpcs, endpoint=False) # Do the thing once with no phi_rf, once with to see difference I = ssfp(T1, T2, TR, alpha, 0, pcs, M0, phi_rf=0) Idf = ssfp(T1, T2, TR, alpha, dfs, 0, M0, phi_rf=0) # Look at the spectral profile pcs_deg = np.rad2deg(pcs) aI = np.angle(I) aIdf = np.unwrap(np.angle(Idf)[::-1])[::-1] fig, ax1 = plt.subplots() ax1.plot(pcs_deg, np.abs(I)) ax1.plot(pcs_deg, np.abs(Idf), '--') ax1.set_ylabel('Magnitude') ax1.set_xlabel('Frequency (deg)') ax2 = ax1.twinx() ax2.plot(pcs_deg, aI, '--', label='Phase, pcs')
TR = 5e-3 alpha = np.deg2rad(10) npcs = 64 pcs = np.linspace(0, 2 * np.pi, npcs, endpoint=False) # pcs = 0 # Tissue parameters T1 = 1.2 T2 = .035 M0 = 1 df = 0 # df = np.linspace(-1/TR, 1/TR, npcs, endpoint=False) phi_rf = np.deg2rad(0) # Simulate acquisition of phase-cycles for a single voxel I = ssfp(T1, T2, TR, alpha, df, pcs, M0, phi_rf=phi_rf) # # Take a gander # fig, ax1 = plt.subplots() # ax2 = ax1.twinx() # ax1.plot(np.abs(I), label='Magnitude') # ax2.plot(np.rad2deg(np.angle(I)), '--', label='Phase') # ax1.legend() # ax1.set_ylabel('Magnitude') # ax2.legend() # ax2.set_ylabel('Phase') # plt.show() # # Make a box # box = np.zeros(npcs) # box[int(npcs/4):-int(npcs/4)] = 1
# Get a 2D image, since we can't seem to replicate using a single # voxel N = 64 df_noise_std = 0 radius = .8 TR = 6e-3 alpha = np.deg2rad(30) npcs = 4 pcs = np.linspace(0, 2*np.pi, npcs, endpoint=False) PD, T1, T2 = cylinder_2d(dims=(N, N), radius=radius) df = 1000 min_df, max_df = -df, df fx = np.linspace(min_df, max_df, N) fy = np.zeros(N) df, _ = np.meshgrid(fx, fy) if df_noise_std > 0: n = np.random.normal(0, df_noise_std, df.shape) df += n I = ssfp(T1, T2, TR, alpha, df, pcs, PD, phi_rf=0) recon = gs_recon(I, pc_axis=0) recon_no_second = gs_recon(I, pc_axis=0, second_pass=False) I0 = np.concatenate(( I, recon[None, ...], recon_no_second[None, ...]), axis=0) view(I0, montage_axis=0)
plt.colorbar() plt.show() csm_mag = np.tile(np.abs(csm), ( npcs, 1, 1, 1, )).transpose((1, 0, 2, 3)) # Do the sim over all coils I = np.zeros((ncoils, npcs, N, N), dtype='complex') for cc in range(ncoils): I[cc, ...] = ssfp(T1s, T2s, TR, alpha, df, pcs, PD, phi_rf=-phi_rf[cc, ...]) # DON'T DO THIS, STUPID! # # phase-cycle correction # Imag = np.abs(I[cc, ...]) # Iphase = np.angle(I[cc, ...]) - np.tile(pcs/2, (N, N, 1)).T # I[cc, ...] = Imag*np.exp(1j*Iphase) I *= csm_mag # view(I.transpose((2, 3, 0, 1))) # Estimate the sensitivity maps from coil images recons = np.zeros((ncoils, N, N), dtype='complex')