def dz_ramp_beta(n, ratio, tb, d1, d2): r"""Creates a sloped beta filter for designing sloped profiles """ ftw = dinf(d1, d2) / tb # fractional transition width shift = n / 4 f = np.array([ 0, shift - (1 + ftw) * tb / 2, shift - (1 - ftw) * tb / 2, shift + (1 - ftw) * tb / 2, shift + (1 + ftw) * tb / 2, n / 2 ]) / (n / 2) # edges, normalized to Nyquist # left amplitude is derived from the fact that the slope we want is # (ratio - 1)/tb, but we specify the amplitudes at the more closely spaced # edges which are (1 - ftw) * tb apart m = np.array([0, 0, (1 - ftw) * (ratio - 1) + 1, 1, 0, 0]) # amp @ edges w = np.array([d1 / d2, 1, d1 / d2]) # band error weights # design the filter, firls requires odd numtaps, so design 1 extra & trim b = signal.firls(n + 1, f, m, w) b = b[0:n] # hilbert transformation to suppress negative passband, and demod to DC b = signal.hilbert(b) b = b * np.exp(-1j * 2 * np.pi / n * shift * np.linspace(0, n - 1, n)) / 2 b = b * np.exp(-1j * np.pi / n * shift) # return a normalized version in order to get 1 at DC return np.expand_dims(b / np.sum(b), 0)
def dzlp(n=64, tb=4, d1=0.01, d2=0.01): di = dinf(d1, d2) w = di / tb f = np.asarray([0, (1 - w) * (tb / 2), (1 + w) * (tb / 2), (n / 2)]) / n m = [1, 0] w = [1, d1 / d2] h = signal.remez(n, f, m, w) return h
def dzmp(n=64, tb=4, d1=0.01, d2=0.01): n2 = 2 * n - 1 di = 0.5 * dinf(2 * d1, 0.5 * d2 * d2) w = di / tb f = np.asarray([0, (1 - w) * (tb / 2), (1 + w) * (tb / 2), (n / 2)]) / n m = [1, 0] w = [1, 2 * d1 / (0.5 * d2 * d2)] hl = signal.remez(n2, f, m, w) h = fmp(hl) return h
def dzls(N=64, tb=4, d1=0.01, d2=0.01): di = dinf(d1, d2) w = di / tb f = np.asarray([0, (1 - w) * (tb / 2), (1 + w) * (tb / 2), (N / 2)]) f = f / (N / 2) m = [1, 1, 0, 0] w = [1, d1 / d2] h = signal.firls(N + 1, f, m, w) # shift the filter half a sample to make it symmetric, like in MATLAB c = np.exp( 1j * 2 * np.pi / (2 * (N + 1)) * np.concatenate([np.arange(0, N / 2 + 1, 1), np.arange(-N / 2, 0, 1)])) h = np.real(np.fft.ifft(np.multiply(np.fft.fft(h), c))) # lop off extra sample h = h[:N] return h
def dz_hadamard_b(n=128, g=5, gind=1, tb=4, d1=0.01, d2=0.01, shift=32): r"""Design a pulse with hadamard encoding Args: n (int): number of time points. g (int): order of the Hadamard matrix. gind (int): index of vector to use from Hadamard matrix for encoding. tb (int): time bandwidth product. d1 (float): passband ripple level in :math:'M_0^{-1}'. d2 (float): stopband ripple level in :math:'M_0^{-1}'. shift (int): n time points shift of pulse. Returns: b (array): SLR beta parameter. References: Souza, S.P., Szumowski, J., Dumoulin, C.L., Plewes, D.P. & Glover, G. 'Sima: Simultaneous multislice acquisition of MR images by hadamard - encoded excitation. J.Comput.Assist.Tomogr. 12, 1026–1030(1988). """ H = linalg.hadamard(g) encode = H[gind - 1, :] ftw = dinf(d1, d2) / tb # fractional transition width of the slab profile if gind == 1: # no sub-slices b = dzls(n, tb, d1, d2) else: # left stopband f = np.asarray([0, shift - (1 + ftw) * (tb / 2)]) m = np.asarray([0, 0]) w = np.asarray([d1 / d2]) # first sub-band ii = 1 gcent = shift + (ii - g / 2 - 1 / 2) * tb / g # first band center # first band left edge f = np.append(f, gcent - (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) if encode[ii - 1] != encode[ii]: # add the first band's right edge and its amplitude, and a weight f = np.append(f, gcent + (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) w = np.append(w, 1) # middle sub-bands for ii in range(2, g): gcent = shift + (ii - g / 2 - 1 / 2) * tb / g # center of band if encode[ii - 1] != encode[ii - 2]: # add a left edge and amp for this band f = np.append(f, gcent - (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) if encode[ii - 1] != encode[ii]: # add a right edge and its amp, and a weight for this band f = np.append(f, gcent + (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) w = np.append(w, 1) # last sub-band ii = g gcent = shift + (ii - g / 2 - 1 / 2) * tb / g # center of last band if encode[ii - 1] != encode[ii - 2]: # add a left edge and amp for the last band f = np.append(f, gcent - (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) # add a right edge and its amp, and a weight for the last band f = np.append(f, gcent + (tb / g / 2 - ftw * (tb / 2))) m = np.append(m, encode[ii - 1]) w = np.append(w, 1) # right stop-band f = np.append(f, (shift + (1 + ftw) * (tb / 2), (n / 2))) / (n / 2) m = np.append(m, [0, 0]) w = np.append(w, d1 / d2) # separate the positive and negative bands mp = (m > 0).astype(float) mn = (m < 0).astype(float) # design the positive and negative filters c = np.exp(1j * 2 * np.pi / (2 * (n + 1)) * np.concatenate( [np.arange(0, n / 2 + 1, 1), np.arange(-n / 2, 0, 1)])) bp = signal.firls(n + 1, f, mp, w) # the positive filter bn = signal.firls(n + 1, f, mn, w) # the negative filter # combine the filters and demodulate b = sp.ifft(np.multiply(sp.fft(bp - bn, center=False), c), center=False) b = np.real(b[:n]) # hilbert transform to suppress negative passband b = signal.hilbert(b) # demodulate to DC c_shift = np.exp(-1j * 2 * np.pi / n * shift * np.arange(0, n, 1)) / 2 c_shift *= np.exp(-1j * np.pi / n * shift) b = np.multiply(b, c_shift) return b
def dz_gslider_b(n=128, g=5, gind=1, tb=4, d1=0.01, d2=0.01, phi=np.pi, shift=32): r"""Design a g-slider pulse b Args: n (int): number of time points. g (int): number of sub-slices. gind (int): subslice index. tb (int): time bandwidth product. d1 (float): passband ripple level in :math:'M_0^{-1}'. d2 (float): stopband ripple level in :math:'M_0^{-1}'. phi (float): subslice phase. shift (int): n time points shift of pulse. Returns: b (array): SLR beta parameter. References: Setsompop, K. et al. 'High-resolution in vivo diffusion imaging of the human brain with generalized slice dithered enhanced resolution: Simultaneous multislice (gSlider-SMS). Magn. Reson. Med.79, 141–151 (2018). """ ftw = dinf(d1, d2) / tb # fractional transition width of the slab profile if np.fmod(g, 2) and gind == int(np.ceil(g / 2)): # centered sub-slice if g == 1: # no sub-slices, as a sanity check b = dzls(n, tb, d1, d2) else: # Design 2 filters, to allow arbitrary phases on the subslice the # first is a wider notch filter with '0's where it the subslice # appears, and the second is the subslice. Multiply the subslice by # its phase and add the filters. f = np.asarray([ 0, (1 / g - ftw) * (tb / 2), (1 / g + ftw) * (tb / 2), (1 - ftw) * (tb / 2), (1 + ftw) * (tb / 2), (n / 2) ]) f = f / (n / 2) m_notch = [0, 0, 1, 1, 0, 0] m_sub = [1, 1, 0, 0, 0, 0] w = [1, 1, d1 / d2] b_notch = signal.firls(n + 1, f, m_notch, w) # the notched filter b_sub = signal.firls(n + 1, f, m_sub, w) # the subslice filter # add them with the subslice phase b = np.add(b_notch, np.multiply(np.exp(1j * phi), b_sub)) # shift the filter half a sample to make it symmetric, # like in MATLAB c = np.exp(1j * 2 * np.pi / (2 * (n + 1)) * np.concatenate( [np.arange(0, n / 2 + 1, 1), np.arange(-n / 2, 0, 1)])) b = sp.ifft(np.multiply(sp.fft(b, center=False), c), center=False) # lop off extra sample b = b[:n] else: # design filters for the slab and the subslice, hilbert xform them # to suppress their left bands, # then demodulate the result back to DC gcent = shift + (gind - g / 2 - 1 / 2) * tb / g if gind > 1 and gind < g: # separate transition bands for slab+slice f = np.asarray([ 0, shift - (1 + ftw) * (tb / 2), shift - (1 - ftw) * (tb / 2), gcent - (tb / g / 2 + ftw * (tb / 2)), gcent - (tb / g / 2 - ftw * (tb / 2)), gcent + (tb / g / 2 - ftw * (tb / 2)), gcent + (tb / g / 2 + ftw * (tb / 2)), shift + (1 - ftw) * (tb / 2), shift + (1 + ftw) * (tb / 2), (n / 2) ]) f = f / (n / 2) m_notch = [0, 0, 1, 1, 0, 0, 1, 1, 0, 0] m_sub = [0, 0, 0, 0, 1, 1, 0, 0, 0, 0] w = [d1 / d2, 1, 1, 1, d1 / d2] elif gind == 1: # the slab and slice share a left transition band f = np.asarray([ 0, shift - (1 + ftw) * (tb / 2), shift - (1 - ftw) * (tb / 2), gcent + (tb / g / 2 - ftw * (tb / 2)), gcent + (tb / g / 2 + ftw * (tb / 2)), shift + (1 - ftw) * (tb / 2), shift + (1 + ftw) * (tb / 2), (n / 2) ]) f = f / (n / 2) m_notch = [0, 0, 0, 0, 1, 1, 0, 0] m_sub = [0, 0, 1, 1, 0, 0, 0, 0] w = [d1 / d2, 1, 1, d1 / d2] elif gind == g: # the slab and slice share a right transition band f = np.asarray([ 0, shift - (1 + ftw) * (tb / 2), shift - (1 - ftw) * (tb / 2), gcent - (tb / g / 2 + ftw * (tb / 2)), gcent - (tb / g / 2 - ftw * (tb / 2)), shift + (1 - ftw) * (tb / 2), shift + (1 + ftw) * (tb / 2), (n / 2) ]) f = f / (n / 2) m_notch = [0, 0, 1, 1, 0, 0, 0, 0] m_sub = [0, 0, 0, 0, 1, 1, 0, 0] w = [d1 / d2, 1, 1, d1 / d2] c = np.exp(1j * 2 * np.pi / (2 * (n + 1)) * np.concatenate( [np.arange(0, n / 2 + 1, 1), np.arange(-n / 2, 0, 1)])) b_notch = signal.firls(n + 1, f, m_notch, w) # the notched filter b_notch = sp.ifft(np.multiply(sp.fft(b_notch, center=False), c), center=False) b_notch = np.real(b_notch[:n]) # hilbert transform to suppress negative passband b_notch = signal.hilbert(b_notch) b_sub = signal.firls(n + 1, f, m_sub, w) # the sub-band filter b_sub = sp.ifft(np.multiply(sp.fft(b_sub, center=False), c), center=False) b_sub = np.real(b_sub[:n]) # hilbert transform to suppress negative passband b_sub = signal.hilbert(b_sub) # add them with the subslice phase b = b_notch + np.exp(1j * phi) * b_sub # demodulate to DC c_shift = np.exp(-1j * 2 * np.pi / n * shift * np.arange(0, n, 1)) / 2 c_shift *= np.exp(-1j * np.pi / n * shift) b = np.multiply(b, c_shift) return b
def dz_b1_rf(dt=2e-6, tb=4, ptype='st', flip=np.pi / 6, pbw=0.3, pbc=2, d1=0.01, d2=0.01, os=8, split_and_reflect=True): """Design a :math:`B_1^{+}`-selective excitation pulse following Grissom \ JMR 2014 Args: dt (float): hardware sampling dwell time in s. tb (int): time-bandwidth product. ptype (string): pulse type, 'st' (small-tip excitation), 'ex' (pi/2 excitation pulse), 'se' (spin-echo pulse), 'inv' (inversion), or 'sat' (pi/2 saturation pulse). flip (float): flip angle, in radians. pbw (float): width of passband in Gauss. pbc (float): center of passband in Gauss. d1 (float): passband ripple level in :math:`M_0^{-1}`. d2 (float): stopband ripple level in :math:`M_0^{-1}`. os (int): matrix scaling factor. split_and_reflect (bool): option to split and reflect designed pulse. Split-and-reflect preserves pulse selectivity when scaled to excite large tip-angles. Returns: 2-element tuple containing - **om1** (*array*): AM waveform. - **dom** (*array*): FM waveform (radians/s). References: Grissom, W., Cao, Z., & Does, M. (2014). :math:`B_1^{+}`-selective excitation pulse design using the Shinnar-Le Roux algorithm. Journal of Magnetic Resonance, 242, 189-196. """ # calculate beta filter ripple [_, d1, d2] = slr.calc_ripples(ptype, d1, d2) # calculate pulse duration b = 4257 * pbw pulse_len = tb / b # calculate number of samples in pulse n = np.int(np.ceil(pulse_len / dt / 2) * 2) if pbc == 0: # we want passband as close to zero as possible. # do my own dual-band filter design to minimize interaction # between the left and right bands # build system matrix A = np.exp(1j * 2 * np.pi * np.outer( np.arange(-n * os / 2, n * os / 2), np.arange(-n / 2, n / 2)) / (n * os)) # build target pattern ii = np.arange(-n * os / 2, n * os / 2) / (n * os) * 2 w = dinf(d1, d2) / tb f = np.asarray([0, (1 - w) * (tb / 2), (1 + w) * (tb / 2), n / 2]) / (n / 2) d = np.double(np.abs(ii) < f[1]) ds = np.double(np.abs(ii) > f[2]) # shift the target pattern to minimum center position pbc = np.int(np.ceil((f[2] - f[1]) * n * os / 2 + f[1] * n * os / 2)) dl = np.roll(d, pbc) dr = np.roll(d, -pbc) dsl = np.roll(ds, pbc) dsr = np.roll(ds, -pbc) # build error weight vector w = dl + dr + d1 / d2 * np.multiply(dsl, dsr) # solve for the dual-band filter AtA = A.conj().T @ np.multiply(np.reshape(w, (np.size(w), 1)), A) Atd = A.conj().T @ np.multiply(w, dr - dl) h = np.imag(np.linalg.pinv(AtA) @ Atd) else: # normal design # design filter h = slr.dzls(n, tb, d1, d2) # dual-band-modulate the filter om = 2 * np.pi * 4257 * pbc # modulation frequency t = np.arange(0, n) * pulse_len / n - pulse_len / 2 h = 2 * h * np.sin(om * t) if split_and_reflect: # split and flip fm waveform to improve large-tip accuracy dom = np.concatenate((h[n // 2::-1], h, h[n:n // 2:-1])) / 2 else: dom = np.concatenate((0 * h[n // 2::-1], h, 0 * h[n:n // 2:-1])) # scale to target flip, convert to Hz dom = dom * flip / (2 * np.pi * dt) # build am waveform om1 = np.concatenate((-np.ones(n // 2), np.ones(n), -np.ones(n // 2))) return om1, dom