def calc_SAR(file: Union[str, Path, Sequence]) -> None: """ Compute Global SAR values on the `.seq` object for head and whole body over the specified time averages. Parameters ---------- file : str, Path or Seuqence `.seq` file for which global SAR values will be computed. Can be path to `.seq` file as `str` or `Path`, or the `Sequence` object itself. Raises ------ ValueError If `file` is a `str` or `Path` to the `.seq` file and this file does not exist on disk. """ if isinstance(file, (str, Path)): if isinstance(file, str): file = Path(file) if file.exists() and file.is_file(): seq_obj = Sequence() seq_obj.read(str(file)) seq_obj = seq_obj else: raise ValueError('Seq file does not exist.') else: seq_obj = file Q_tmf, Q_hmf = _load_Q() SAR_wbg, SAR_hg, t = _SAR_from_seq(seq_obj, Q_tmf, Q_hmf) SARwbg_lim, tsec = _SAR_interp(SAR_wbg, t) SARhg_lim, tsec = _SAR_interp(SAR_hg, t) SAR_wbg_tensec, SAR_wbg_sixmin, SAR_hg_tensec, SAR_hg_sixmin, SAR_wbg_sixmin_peak, SAR_hg_sixmin_peak, \ SAR_wbg_tensec_peak, SAR_hg_tensec_peak = _SAR_lims_check(SARwbg_lim, SARhg_lim, tsec) # Plot 10 sec average SAR if (tsec[-1] > 10): plt.plot(tsec, SAR_wbg_tensec, 'x-', label='Whole Body: 10sec') plt.plot(tsec, SAR_hg_tensec, '.-', label='Head only: 10sec') # plt.plot(t, SARwbg, label='Whole Body - instant') # plt.plot(t, SARhg, label='Whole Body - instant') plt.xlabel('Time (s)') plt.ylabel('SAR (W/kg)') plt.title('Global SAR - Mass Normalized - Whole body and head only') plt.legend() plt.grid(True) plt.show()
def read_any_version(seq_file: Union[str, Path], seq: Sequence = None) \ -> Sequence: """ Reads a sequence file (seq_file) independent of the (py)pulseq version. :param seq_file: path to the sequence file to read into the Sequence object :param seq: the sequence to read the seq file into. If not provided, a new Sequence object is instantiated :return seq: Sequence object """ version = get_minor_version(seq_file) if not seq: seq = Sequence() if version in [2, 3]: seq.read(seq_file) else: raise ValueError('Version', version, 'can not be converted.') return seq
def waveform_from_seqblock(seq_block): """ extracts gradient waveform from Pypulseq sequence block """ from pypulseq.Sequence.sequence import Sequence if seq_block.channel == 'x': axis = 0 elif seq_block.channel == 'y': axis = 1 elif seq_block.channel == 'z': axis = 2 else: raise ValueError('No valid gradient waveform') dummy_seq = Sequence() # helper dummy sequence dummy_seq.add_block(seq_block) return dummy_seq.gradient_waveforms()[ axis, :-1] # last value is a zero that does not belong to the waveform
def SARfromseq(fname, Qtmf, Qhmf): """ This definition computes the global whole body and head only SAR values Parameters ---------- fname : str Qtmf : numpy.ndarray Qhmf : numpy.ndarray Returns ------- SARwbg_vec : numpy.ndarray SARhg_vec : numpy.ndarray t_vec : numpy.ndarray contains the Q-matrix, GSAR head and body for now """ obj = Sequence() obj.read(str(SAR_PATH / 'assets' / fname)) # replaced by # Identify rf blocks and compute SAR - 10 seconds must be less than twice and 6 minutes must be less than 4 (WB) and 3.2 (head-20) blockEvents = obj.block_events numEvents = len(blockEvents) t_vec = np.zeros(numEvents) SARwbg_vec = np.zeros(t_vec.shape) SARhg_vec = np.zeros(t_vec.shape) t_prev = 0 for iB in blockEvents: block = obj.get_block(iB) block_dur = calc_duration(block) t_vec[iB - 1] = t_prev + block_dur t_prev = t_vec[iB - 1] if ('rf' in block): # has rf rf = block['rf'] t = rf.t signal = rf.signal # This rf could be parallel transmit as well SARwbg_vec[iB] = calc_SAR(Qtmf, signal) SARhg_vec[iB] = calc_SAR(Qhmf, signal) return SARwbg_vec, SARhg_vec, t_vec
def calc_SAR(seq): """ Compute Global SAR values on the `seq` object for head and whole body over the specified time averages. Parameters ---------- seq : Sequence pypulseq `Sequence` object for which global SAR values will be computed. """ if isinstance(seq, str): seq_obj = Sequence() seq_obj.read(seq) seq = seq_obj Qtmf, Qhmf = __loadQ() SARwbg, SARhg, t_vec = __SAR_from_seq(seq, Qtmf, Qhmf) SARwbg_lim, tsec = __SAR_interp(SARwbg, t_vec) SARhg_lim, tsec = __SAR_interp(SARhg, t_vec) SAR_wbg_tensec, SAR_wbg_sixmin, SAR_hg_tensec, SAR_hg_sixmin, SAR_wbg_sixmin_peak, SAR_hg_sixmin_peak, SAR_wbg_tensec_peak, SAR_hg_tensec_peak = __SAR_lims_check( SARwbg_lim, SARhg_lim, tsec) # Plot 10 sec average SAR if (tsec[-1] > 10): plt.plot(tsec, SAR_wbg_tensec, label='Whole Body: 10sec') plt.plot(tsec, SAR_hg_tensec, label='Head only: 10sec') # plt.plot(t_vec, SARwbg, label='Whole Body - instant') # plt.plot(t_vec, SARhg, label='Whole Body - instant') plt.xlabel('Time (s)') plt.ylabel('SAR (W/kg)') plt.title('Global SAR - Mass Normalized - Whole body and head only') ax = plt.subplot(111) # ax.legend() plt.grid(True) plt.show()
def make_pulseq_epi_oblique(fov, n, thk, fa, tr, te, enc='xyz', slice_locs=None, echo_type="se", n_shots=1, seg_type='blocked', write=False): """Makes an Echo Planar Imaging (EPI) sequence in any plane 2D oblique multi-slice EPI pulse sequence with Cartesian encoding Oblique means that each of slice-selection, phase encoding, and frequency encoding can point in any specified direction Parameters ---------- fov : array_like Isotropic field-of-view, or length-2 list [fov_readout, fov_phase], in meters n : array_like Isotropic matrix size, or length-2 list [n_readout, n_phase] thk : float Slice thickness in meters fa : float Flip angle in degrees tr : float Repetition time in seconds te : float Echo time in seconds enc : str or array_like, optional Spatial encoding directions 1st - readout; 2nd - phase encoding; 3rd - slice select - Use str with any permutation of x, y, and z to obtain orthogonal slices e.g. The default 'xyz' means axial(z) slice with readout in x and phase encoding in y - Use list to indicate vectors in the encoding directions for oblique slices They should be perpendicular to each other, but not necessarily unit vectors e.g. [(2,1,0),(-1,2,0),(0,0,1)] rotates the two in-plane encoding directions for an axial slice slice_locs : array_like, optional Slice locations from isocenter in meters Default is None which means a single slice at the center echo_type : str, optional {'se','gre'} Type of echo generated se (default) - spin echo (an 180 deg pulse is used) gre - gradient echo n_shots : int, optional Number of shots used to encode each slicel; default is 1 seg_type : str, optional {'blocked','interleaved'} Method to divide up k-space in the case of n_shots > 1; default is 'blocked' 'blocked' - each shot covers a rectangle, with no overlap between shots 'interleaved' - each shot samples the full k-space but with wider phase steps write : bool, optional Whether to write seq into file; default is False Returns ------- seq : Sequence Pulse sequence as a Pulseq object ro_dirs : numpy.ndarray List of 0s and 1s indicating direction of readout 0 - left to right 1 - right to left (needs to be reversed at recon) ro_order : numpy.ndarray Order in which to re-arrange the readout lines It is [] for blocked acquisition (retain original order) """ # Multi-slice, multi-shot (>=1) # TE is set to be where the trajectory crosses the center of k-space # System options kwargs_for_opts = {'max_grad': 32, 'grad_unit': 'mT/m', 'max_slew': 130, 'slew_unit': 'T/m/s', 'rf_ring_down_time': 30e-6, 'rf_dead_time': 100e-6, 'adc_dead_time': 20e-6} system = Opts(kwargs_for_opts) seq = Sequence(system) ug_fe, ug_pe, ug_ss = parse_enc(enc) # Sequence parameters Nf, Np = (n, n) if isinstance(n, int) else (n[0], n[1]) delta_k_ro, delta_k_pe = (1 / fov, 1 / fov) if isinstance(fov, float) else (1 / fov[0], 1 / fov[1]) kWidth_ro = Nf * delta_k_ro TE, TR = te, tr flip = fa * pi / 180 # RF Pulse (first) kwargs_for_sinc = {"flip_angle": flip, "system": system, "duration": 2.5e-3, "slice_thickness": thk, "apodization": 0.5, "time_bw_product": 4} rf, g_ss = make_sinc_pulse(kwargs_for_sinc, 2) g_ss_x, g_ss_y, g_ss_z = make_oblique_gradients(g_ss, ug_ss) # Readout gradients # readoutTime = Nf * 4e-6 dwell = 1e-5 readoutTime = Nf * dwell kwargs_for_g_ro = {"channel": 'x', "system": system, "flat_area": kWidth_ro, "flat_time": readoutTime} g_ro_pos = make_trapezoid(kwargs_for_g_ro) g_ro_pos_x, g_ro_pos_y, g_ro_pos_z = make_oblique_gradients(g_ro_pos, ug_fe) g_ro_neg = copy.deepcopy(g_ro_pos) modify_gradient(g_ro_neg, scale=-1) g_ro_neg_x, g_ro_neg_y, g_ro_neg_z = make_oblique_gradients(g_ro_neg, ug_fe) kwargs_for_adc = {"num_samples": Nf, "system": system, "duration": g_ro_pos.flat_time, "delay": g_ro_pos.rise_time + dwell / 2} adc = makeadc(kwargs_for_adc) pre_time = 8e-4 # 180 deg pulse for SE if echo_type == "se": # RF Pulse (180 deg for SE) flip180 = 180 * pi / 180 kwargs_for_sinc = {"flip_angle": flip180, "system": system, "duration": 2.5e-3, "slice_thickness": thk, "apodization": 0.5, "time_bw_product": 4} rf180, g_ss180 = make_sinc_pulse(kwargs_for_sinc, 2) # Slice-select direction spoilers kwargs_for_g_ss_spoil = {"channel": 'z', "system": system, "area": g_ss.area * 2, "duration": 3 * pre_time} g_ss_spoil = make_trapezoid(kwargs_for_g_ss_spoil) ## modify_gradient(g_ss_spoil, 0) ## g_ss_spoil_x, g_ss_spoil_y, g_ss_spoil_z = make_oblique_gradients(g_ss_spoil, ug_ss) # Readout rewinder ro_pre_area = g_ro_neg.area / 2 if echo_type == 'gre' else g_ro_pos.area / 2 kwargs_for_g_ro_pre = {"channel": 'x', "system": system, "area": ro_pre_area, "duration": pre_time} g_ro_pre = make_trapezoid(kwargs_for_g_ro_pre) g_ro_pre_x, g_ro_pre_y, g_ro_pre_z = make_oblique_gradients(g_ro_pre, ug_fe) # Slice-selective rephasing kwargs_for_g_ss_reph = {"channel": 'z', "system": system, "area": -g_ss.area / 2, "duration": pre_time} g_ss_reph = make_trapezoid(kwargs_for_g_ss_reph) g_ss_reph_x, g_ss_reph_y, g_ss_reph_z = make_oblique_gradients(g_ss_reph, ug_ss) # Phase encode rewinder if echo_type == 'gre': pe_max_area = (Np / 2) * delta_k_pe elif echo_type == 'se': pe_max_area = -(Np / 2) * delta_k_pe kwargs_for_g_pe_max = {"channel": 'y', "system": system, "area": pe_max_area, "duration": pre_time} g_pe_max = make_trapezoid(kwargs_for_g_pe_max) # Phase encoding blips dur = ceil(2 * sqrt(delta_k_pe / system.max_slew) / 10e-6) * 10e-6 kwargs_for_g_blip = {"channel": 'y', "system": system, "area": delta_k_pe, "duration": dur} g_blip = make_trapezoid(kwargs_for_g_blip) # Delays duration_to_center = (Np / 2) * calc_duration(g_ro_pos) + (Np - 1) / 2 * calc_duration(g_blip) # why? if echo_type == 'se': delayTE1 = TE / 2 - calc_duration(g_ss) / 2 - pre_time - calc_duration(g_ss_spoil) - calc_duration(rf180) / 2 delayTE2 = TE / 2 - calc_duration(rf180) / 2 - calc_duration(g_ss_spoil) - duration_to_center delay1 = make_delay(delayTE1) delay2 = make_delay(delayTE2) elif echo_type == 'gre': delayTE = TE - calc_duration(g_ss) / 2 - pre_time - duration_to_center delay12 = make_delay(delayTE) delayTR = TR - TE - calc_duration(rf) / 2 - duration_to_center delay3 = make_delay(delayTR) # This might be different for each rep though. Fix later ##################################################################################################### # Multi-shot calculations ro_dirs = [] ro_order = [] # Find number of lines in each block if seg_type == 'blocked': # Number of lines in each full readout block nl = ceil(Np / n_shots) # Number of k-space lines per readout if Np % nl == 0: nlines_list = nl * np.ones(n_shots) else: nlines_list = nl * np.ones(n_shots - 1) nlines_list = np.append(nlines_list, Np % nl) pe_scales = 2 * np.append([0], np.cumsum(nlines_list)[:-1]) / Np - 1 g_blip_x, g_blip_y, g_blip_z = make_oblique_gradients(g_blip, ug_pe) for nlines in nlines_list: ro_dirs = np.append(ro_dirs, ((-1) ** (np.arange(0, nlines) + 1) + 1) / 2) elif seg_type == 'interleaved': # Minimum number of lines per readout nb = floor(Np / n_shots) # Number of k-space lines per readout nlines_list = np.ones(n_shots) * nb nlines_list[:Np % n_shots] += 1 # Phase encoding scales (starts from -1; i.e. bottom left combined with pre-readout) pe_scales = 2 * np.arange(0, (Np - n_shots) / Np, 1 / Np)[0:n_shots] - 1 print(pe_scales) # Larger blips modify_gradient(g_blip, scale=n_shots) g_blip_x, g_blip_y, g_blip_z = make_oblique_gradients(g_blip, ug_pe) # ro_order = np.reshape(np.reshape(np.arange(0,Np),(),order='F'),(0,Np)) ro_order = np.zeros((nb + 1, n_shots)) ro_inds = np.arange(Np) # Readout order for recon for k in range(n_shots): cs = int(nlines_list[k]) ro_order[:cs, k] = ro_inds[:cs] ro_inds = np.delete(ro_inds, range(cs)) ro_order = ro_order.flatten()[:Np].astype(int) print(ro_order) # Readout directions in original (interleaved) order for nlines in nlines_list: ro_dirs = np.append(ro_dirs, ((-1) ** (np.arange(0, nlines) + 1) + 1) / 2) ##################################################################################################### # Add blocks for u in range(len(slice_locs)): # For each slice # Offset rf rf.freq_offset = g_ss.amplitude * slice_locs[u] for v in range(n_shots): # Find init. phase encode g_pe = copy.deepcopy(g_pe_max) modify_gradient(g_pe, pe_scales[v]) g_pe_x, g_pe_y, g_pe_z = make_oblique_gradients(g_pe, ug_pe) # First RF seq.add_block(rf, g_ss_x, g_ss_y, g_ss_z) # Pre-winder gradients pre_grads_list = [g_ro_pre_x, g_ro_pre_y, g_ro_pre_z, g_pe_x, g_pe_y, g_pe_z, g_ss_reph_x, g_ss_reph_y, g_ss_reph_z] gtx, gty, gtz = combine_trap_grad_xyz(pre_grads_list, system, pre_time) seq.add_block(gtx, gty, gtz) # 180 deg pulse and spoilers, only for Spin Echo if echo_type == 'se': # First delay seq.add_block(delay1) # Second RF : 180 deg with spoilers on both sides seq.add_block(g_ss_spoil_x, g_ss_spoil_y, g_ss_spoil_z) # why? seq.add_block(rf180) seq.add_block(g_ss_spoil_x, g_ss_spoil_y, g_ss_spoil_z) # Delay between rf180 and beginning of readout seq.add_block(delay2) # For gradient echo it's just a delay elif echo_type == 'gre': seq.add_block(delay12) # EPI readout with blips for i in range(int(nlines_list[v])): if i % 2 == 0: seq.add_block(g_ro_pos_x, g_ro_pos_y, g_ro_pos_z, adc) # ro line in the positive direction else: seq.add_block(g_ro_neg_x, g_ro_neg_y, g_ro_neg_z, adc) # ro line backwards seq.add_block(g_blip_x, g_blip_y, g_blip_z) # blip seq.add_block(delay3) # Display 1 TR # seq.plot(time_range=(0, TR)) if write: seq.write("epi_{}_FOVf{:.0f}mm_FOVp{:.0f}mm_Nf{:d}_Np{:d}_TE{:.0f}ms_TR{:.0f}ms_{:d}shots.seq" \ .format(echo_type, fov[0] * 1000, fov[1] * 1000, Nf, Np, TE * 1000, TR * 1000, n_shots)) print('EPI sequence (oblique) constructed') return seq, ro_dirs, ro_order
def make_pulseq_se_oblique(fov, n, thk, fa, tr, te, enc='xyz', slice_locs=None, write=False): """Makes a Spin Echo (SE) sequence in any plane 2D oblique multi-slice Spin-Echo pulse sequence with Cartesian encoding Oblique means that each of slice-selection, phase encoding, and frequency encoding can point in any specified direction Parameters ---------- fov : array_like Isotropic field-of-view, or length-2 list [fov_readout, fov_phase], in meters n : array_like Isotropic matrix size, or length-2 list [n_readout, n_phase] thk : float Slice thickness in meters fa : float Flip angle in degrees tr : float Repetition time in seconds te : float Echo time in seconds enc : str or array_like, optional Spatial encoding directions 1st - readout; 2nd - phase encoding; 3rd - slice select - Use str with any permutation of x, y, and z to obtain orthogonal slices e.g. The default 'xyz' means axial(z) slice with readout in x and phase encoding in y - Use list to indicate vectors in the encoding directions for oblique slices They should be perpendicular to each other, but not necessarily unit vectors e.g. [(2,1,0),(-1,2,0),(0,0,1)] rotates the two in-plane encoding directions for an axial slice slice_locs : array_like, optional Slice locations from isocenter in meters Default is None which means a single slice at the center write : bool, optional Whether to write seq into file; default is False Returns ------- seq : Sequence Pulse sequence as a Pulseq object """ # System options kwargs_for_opts = {'max_grad': 32, 'grad_unit': 'mT/m', 'max_slew': 130, 'slew_unit': 'T/m/s', 'rf_ring_down_time': 30e-6, 'rf_dead_time': 100e-6, 'adc_dead_time': 20e-6} system = Opts(kwargs_for_opts) seq = Sequence(system) # Sequence parameters ug_fe, ug_pe, ug_ss = parse_enc(enc) Nf, Np = (n, n) if isinstance(n, int) else (n[0], n[1]) delta_k_ro, delta_k_pe = (1 / fov, 1 / fov) if isinstance(fov, float) else (1 / fov[0], 1 / fov[1]) kWidth_ro = Nf * delta_k_ro TE, TR = te, tr # Non-180 pulse flip1 = fa * pi / 180 kwargs_for_sinc = {"flip_angle": flip1, "system": system, "duration": 2e-3, "slice_thickness": thk, "apodization": 0.5, "time_bw_product": 4} rf, g_ss = make_sinc_pulse(kwargs_for_sinc, 2) g_ss_x, g_ss_y, g_ss_z = make_oblique_gradients(g_ss, ug_ss) # 180 pulse flip2 = 180 * pi / 180 kwargs_for_sinc = {"flip_angle": flip2, "system": system, "duration": 2e-3, "slice_thickness": thk, "apodization": 0.5, "time_bw_product": 4} rf180, g_ss180 = make_sinc_pulse(kwargs_for_sinc, 2) g_ss180_x, g_ss180_y, g_ss180_z = make_oblique_gradients(g_ss180, ug_ss) # Readout gradient & ADC readoutTime = 6.4e-3 kwargs_for_g_ro = {"channel": 'x', "system": system, "flat_area": kWidth_ro, "flat_time": readoutTime} g_ro = make_trapezoid(kwargs_for_g_ro) g_ro_x, g_ro_y, g_ro_z = make_oblique_gradients(g_ro, ug_fe) kwargs_for_adc = {"num_samples": Nf, "system": system, "duration": g_ro.flat_time, "delay": g_ro.rise_time} adc = makeadc(kwargs_for_adc) # RO rewinder gradient kwargs_for_g_ro_pre = {"channel": 'x', "system": system, "area": g_ro.area / 2, "duration": 2e-3} g_ro_pre = make_trapezoid(kwargs_for_g_ro_pre) g_ro_pre_x, g_ro_pre_y, g_ro_pre_z = make_oblique_gradients(g_ro_pre, ug_fe) # Slice refocusing gradient kwargs_for_g_ss_reph = {"channel": 'z', "system": system, "area": -g_ss.area / 2, "duration": 2e-3} g_ss_reph = make_trapezoid(kwargs_for_g_ss_reph) g_ss_reph_x, g_ss_reph_y, g_ss_reph_z = make_oblique_gradients(g_ss_reph, ug_ss) # Delays delayTE1 = (TE - 2 * max(calc_duration(g_ss_reph), calc_duration(g_ro_pre)) - calc_duration(g_ss) - calc_duration( g_ss180)) / 2 delayTE2 = (TE - calc_duration(g_ro) - calc_duration(g_ss180)) / 2 delayTE3 = TR - TE - (calc_duration(g_ss) + calc_duration(g_ro)) / 2 delay1 = make_delay(delayTE1) delay2 = make_delay(delayTE2) delay3 = make_delay(delayTE3) # Construct sequence if slice_locs is None: locs = [0] else: locs = slice_locs for u in range(len(locs)): rf180.freq_offset = g_ss180.amplitude * locs[u] rf.freq_offset = g_ss.amplitude * locs[u] for i in range(Np): seq.add_block(rf, g_ss_x, g_ss_y, g_ss_z) # 90-deg pulse kwargs_for_g_pe = {"channel": 'y', "system": system, "area": -(Np / 2 - i) * delta_k_pe, "duration": 2e-3} g_pe = make_trapezoid(kwargs_for_g_pe) # Phase encoding gradient g_pe_x, g_pe_y, g_pe_z = make_oblique_gradients(g_pe, ug_pe) pre_grads_list = [g_ro_pre_x, g_ro_pre_y, g_ro_pre_z, g_ss_reph_x, g_ss_reph_y, g_ss_reph_z, g_pe_x, g_pe_y, g_pe_z] gtx, gty, gtz = combine_trap_grad_xyz(pre_grads_list, system, 2e-3) seq.add_block(gtx, gty, gtz) # Add a combination of ro rewinder, phase encoding, and slice refocusing seq.add_block(delay1) # Delay 1: until 180-deg pulse seq.add_block(rf180, g_ss180_x, g_ss180_y, g_ss180_z) # 180 deg pulse for SE seq.add_block(delay2) # Delay 2: until readout seq.add_block(g_ro_x, g_ro_y, g_ro_z, adc) # Readout! seq.add_block(delay3) # Delay 3: until next inversion pulse if write: seq.write( "se_fov{:.0f}mm_Nf{:d}_Np{:d}_TE{:.0f}ms_TR{:.0f}ms.seq".format(fov * 1000, Nf, Np, TE * 1000, TR * 1000)) print('Spin echo sequence (oblique) constructed') return seq
def make_pulseq_se(fov, n, thk, fa, tr, te, enc='xyz', slice_locs=None, write=False): """Makes a Spin Echo (SE) sequence 2D orthogonal multi-slice Spin-Echo pulse sequence with Cartesian encoding Orthogonal means that each of slice-selection, phase encoding, and frequency encoding aligns with the x, y, or z directions Parameters ---------- fov : float Field-of-view in meters (isotropic) n : int Matrix size (isotropic) thk : float Slice thickness in meters fa : float Flip angle in degrees tr : float Repetition time in seconds te : float Echo time in seconds enc : str, optional Spatial encoding directions 1st - readout; 2nd - phase encoding; 3rd - slice select Default 'xyz' means axial(z) slice with readout in x and phase encoding in y slice_locs : array_like, optional Slice locations from isocenter in meters Default is None which means a single slice at the center write : bool, optional Whether to write seq into file; default is False Returns ------- seq : Sequence Pulse sequence as a Pulseq object """ kwargs_for_opts = {"max_grad": 33, "grad_unit": "mT/m", "max_slew": 100, "slew_unit": "T/m/s", "rf_dead_time": 10e-6, "adc_dead_time": 10e-6} system = Opts(kwargs_for_opts) seq = Sequence(system) # Parameters Nf = n Np = n delta_k = 1 / fov kWidth = Nf * delta_k TE, TR = te, tr # Non-180 pulse flip1 = fa * pi / 180 kwargs_for_sinc = {"flip_angle": flip1, "system": system, "duration": 2e-3, "slice_thickness": thk, "apodization": 0.5, "time_bw_product": 4} rf, g_ss = make_sinc_pulse(kwargs_for_sinc, 2) g_ss.channel = enc[2] # 180 pulse flip2 = 180 * pi / 180 kwargs_for_sinc = {"flip_angle": flip2, "system": system, "duration": 2e-3, "slice_thickness": thk, "apodization": 0.5, "time_bw_product": 4} rf180, g_ss180 = make_sinc_pulse(kwargs_for_sinc, 2) g_ss180.channel = enc[2] # Readout gradient & ADC # readoutTime = system.grad_raster_time * Nf readoutTime = 6.4e-3 kwargs_for_g_ro = {"channel": enc[0], "system": system, "flat_area": kWidth, "flat_time": readoutTime} g_ro = make_trapezoid(kwargs_for_g_ro) kwargs_for_adc = {"num_samples": Nf, "system": system, "duration": g_ro.flat_time, "delay": g_ro.rise_time} adc = makeadc(kwargs_for_adc) # RO rewinder gradient kwargs_for_g_ro_pre = {"channel": enc[0], "system": system, "area": g_ro.area / 2, "duration": 2e-3} # "duration": g_ro.rise_time + g_ro.fall_time + readoutTime / 2} g_ro_pre = make_trapezoid(kwargs_for_g_ro_pre) # Slice refocusing gradient kwargs_for_g_ss_reph = {"channel": enc[2], "system": system, "area": -g_ss.area / 2, "duration": 2e-3} g_ss_reph = make_trapezoid(kwargs_for_g_ss_reph) # Delays delayTE1 = (TE - 2 * max(calc_duration(g_ss_reph), calc_duration(g_ro_pre)) - calc_duration(g_ss) - calc_duration( g_ss180)) / 2 # delayTE2 = TE / 2 - calc_duration(g_ro) / 2 - calc_duration(g_ss180) / 2 delayTE2 = (TE - calc_duration(g_ro) - calc_duration(g_ss180)) / 2 delayTE3 = TR - TE - (calc_duration(g_ss) + calc_duration(g_ro)) / 2 delay1 = make_delay(delayTE1) delay2 = make_delay(delayTE2) delay3 = make_delay(delayTE3) # Construct sequence if slice_locs is None: locs = [0] else: locs = slice_locs for u in range(len(locs)): rf180.freq_offset = g_ss180.amplitude * locs[u] rf.freq_offset = g_ss.amplitude * locs[u] for i in range(Np): seq.add_block(rf, g_ss) # 90-deg pulse kwargs_for_g_pe_pre = {"channel": enc[1], "system": system, "area": -(Np / 2 - i) * delta_k, "duration": 2e-3} # "duration": g_ro.rise_time + g_ro.fall_time + readoutTime / 2} g_pe_pre = make_trapezoid(kwargs_for_g_pe_pre) # Phase encoding gradient seq.add_block(g_ro_pre, g_pe_pre, g_ss_reph) # Add a combination of ro rewinder, phase encoding, and slice refocusing seq.add_block(delay1) # Delay 1: until 180-deg pulse seq.add_block(rf180, g_ss180) # 180 deg pulse for SE seq.add_block(delay2) # Delay 2: until readout seq.add_block(g_ro, adc) # Readout! seq.add_block(delay3) # Delay 3: until next inversion pulse if write: seq.write( "se_fov{:.0f}mm_Nf{:d}_Np{:d}_TE{:.0f}ms_TR{:.0f}ms.seq".format(fov * 1000, Nf, Np, TE * 1000, TR * 1000)) print('Spin echo sequence constructed') return seq
import numpy as np from pypulseq.Sequence.sequence import Sequence from pypulseq.calc_rf_center import calc_rf_center from pypulseq.make_adc import make_adc from pypulseq.make_delay import make_delay from pypulseq.make_extended_trapezoid import make_extended_trapezoid from pypulseq.make_sinc_pulse import make_sinc_pulse from pypulseq.make_trap_pulse import make_trapezoid from pypulseq.opts import Opts dG = 250e-6 system = Opts(max_grad=30, grad_unit='mT/m', max_slew=170, slew_unit='T/m/s', rf_ringdown_time=100e-6, rf_dead_time=100e-6, adc_dead_time=10e-6) seq = Sequence(system=system) fov = 256e-3 Ny_pre = 8 Nx, Ny = 128, 128 n_echo = int(Ny / 2 + Ny_pre) n_slices = 1 rf_flip = 180 if isinstance(rf_flip, int): rf_flip = np.zeros(n_echo) + rf_flip slice_thickness = 5e-3 TE = 12e-3 TR = 2000e-3 TE_eff = 60e-3 k0 = round(TE_eff / TE) PE_type = 'linear'
from pypulseq.Sequence.sequence import Sequence import matplotlib.pyplot as plt import h5py # Load both files q1 = 3 q2 = 6 seq_orig = Sequence() seq_orig.read('gre_jemris.seq') print('Seq original') print(seq_orig.get_block(q1).gx) seq_proc = Sequence() seq_proc.read('gre_jemris_seq2xml_jemris.seq') #print("Seq processed:") #print(seq_proc.get_block(q2).gx) #seq_orig.plot(time_range=[0,10]) #seq_proc.plot(time_range=[0,10]) sd = h5py.File('gre_jemris_seq2xml_jemris.h5', 'r') sd = sd['seqdiag'] plt.figure(1) plt.subplot(411) plt.title("Twice converted JEMRIS sequence diagram") plt.plot(sd['T'][()], sd['TXM'][()])
return rf_inv # Press the green button in the gutter to run the script. if __name__ == '__main__': # System limits system = Opts(max_grad=28, grad_unit='mT/m', max_slew=125, slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=10e-6) seq = Sequence(system) # Sequence Parameters FOV = 200e-3 # [m] Ny = 128 Nstartup = 11 # Calculate TR _, TR, Ny_aq = bssfp_readout(Sequence(system), system, fov=FOV, Nstartup=Nstartup, Ny=Ny) # Calculate Crushers gzSpoil_INV = make_trapezoid('z',
# ====== # SETUP # ====== dG = 250e-6 # Set system limits system = Opts(max_grad=30, grad_unit='mT/m', max_slew=170, slew_unit='T/m/s', rf_ringdown_time=100e-6, rf_dead_time=100e-6, adc_dead_time=10e-6) seq = Sequence(system=system) # Create a new sequence object # Define FOV and resolution fov = 256e-3 Ny_pre = 8 Nx, Ny = 128, 128 n_echo = int(Ny / 2 + Ny_pre) n_slices = 1 rf_flip = 180 if isinstance(rf_flip, int): rf_flip = np.zeros(n_echo) + rf_flip slice_thickness = 5e-3 # Slice thickness TE = 12e-3 TR = 2000e-3 TE_eff = 60e-3 k0 = round(TE_eff / TE) PE_type = 'linear'
from pypulseq.Sequence.sequence import Sequence from pypulseq.opts import Opts from pypulseq.make_arbitrary_grad import make_arbitrary_grad import numpy as np seq = Sequence(system=Opts()) gx = make_arbitrary_grad(channel='x', waveform=np.array([1, 2, 3, 4, 5, 4, 3, 2, 1])) seq.add_block(gx) seq.write("hiseq.seq") seq2 = Sequence() seq2.read('hiseq.seq') print(seq2.get_block(1).gx)
def make_code_sequence(FOV=250e-3, N=64, TR=100e-3, flip=15, enc_type='3D', rf_type='gauss', os_factor=1, save_seq=True): """ 3D or 2D (projection) CODE sequence (cite! ) Parameters ---------- FOV : float, default=0.25 Isotropic image field-of-view in [meters] N : int, default=64 Isotropic image matrix size. Also used for base readout sample number (2x that of Nyquist requirement) TR : float, default=0.1 Repetition time in [seconds] flip : float, default=15 Flip angle in [degrees] enc_type : str, default='3D' Dimensionality of sequence - '3D' or '2D' rf_type : str, default='gauss' RF pulse shape - 'gauss' or 'sinc' os_factor : float, default=1 Oversampling factor in readout The number of readout samples is the nearest integer from os_factor*N save_seq : bool, default=True Whether to save this sequence as a .seq file Returns ------- seq : Sequence PyPulseq CODE sequence object TE : float Echo time of generated sequence in [seconds] ktraj : np.ndarray 3D k-space trajectory [spoke, readout sample, 3] where the last dimension refers to spatial frequency coordinates (kx, ky, kz) For 2D projection version, disregard the last dimension. """ # System options (copied from : amri-sos service form) system = Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6, adc_dead_time=20e-6) # Parameters # Radial sampling set-up dx = FOV / N if enc_type == '3D': Ns, Ntheta, Nphi = get_radk_params_3D(dx, FOV) # Radial encoding details thetas = np.linspace(0, np.pi, Ntheta, endpoint=False) elif enc_type == "2D": Nphi = get_radk_params_2D(N) Ntheta = 1 Ns = Nphi thetas = np.array([np.pi / 2]) # Zero z-gradient. phis = np.linspace(0, 2 * np.pi, Nphi, endpoint=False) print( f'{enc_type} acq.: using {Ntheta} thetas and {Nphi} phis - {Ns} spokes in total.' ) # Make sequence components # Slice-selective RF pulse: 100 us, 15 deg gauss pulse FA = flip * pi / 180 rf_dur = 100e-6 thk_slab = FOV if rf_type == 'gauss': rf, g_pre, __ = make_gauss_pulse(flip_angle=FA, duration=rf_dur, slice_thickness=thk_slab, system=system, return_gz=True) elif rf_type == 'sinc': rf, g_pre, __ = make_sinc_pulse(flip_angle=FA, duration=rf_dur, slice_thickness=thk_slab, apodization=0.5, time_bw_product=4, system=system, return_gz=True) else: raise ValueError("RF type can only be sinc or gauss") # Round off timing to system requirements rf.delay = system.grad_raster_time * round( rf.delay / system.grad_raster_time) g_pre.delay = system.grad_raster_time * round( g_pre.delay / system.grad_raster_time) # Readout gradient (5 ms readout time) #ro_time = 5e-3 #ro_rise_time = 20e-6 dr = FOV / N kmax = (1 / dr) / 2 Nro = np.round(os_factor * N) # Asymmetry! (0 - fully rewound; 1 - half-echo) ro_asymmetry = (kmax - g_pre.area / 2) / (kmax + g_pre.area / 2) s = np.round(ro_asymmetry * Nro / 2) / (Nro / 2) ro_duration = 2.5e-3 dkp = (1 / FOV) / (1 + s) ro_area = N * dkp g_ro = make_trapezoid(channel='x', flat_area=ro_area, flat_time=ro_duration, system=system) adc = make_adc(num_samples=Nro, duration=g_ro.flat_time, delay=g_ro.rise_time, system=system) # Readout gradient & ADC #adc_dwell = 10e-6 # 10 us sampling interval (100 KHz readout bandwidth) #g_ro = make_trapezoid(channel='x', system=system, amplitude=dk/adc_dwell, flat_time=adc_dwell*int(N/2), rise_time=ro_rise_time) #flat_delay = (0.5*g_pre.area - 0.5*ro_rise_time*g_ro.amplitude) / g_ro.amplitude #flat_delay = system.grad_raster_time * round(flat_delay / system.grad_raster_time) # Make sure the first ADC is at center of k-space. #adc = make_adc(system=system, num_samples=Nro, dwell = adc_dwell, delay = g_ro.rise_time+flat_delay) # Delay TRfill = TR - calc_duration(g_pre) - calc_duration(g_ro) delayTR = make_delay(TRfill) #TE = 0.5 * calc_duration(g_pre) + adc.delay TE = 0.5 * calc_duration(g_pre) + g_ro.rise_time + adc.dwell * Nro / 2 * ( 1 - s) print(f'TE obtained: {TE*1e3} ms') # Initiate storage of trajectory ktraj = np.zeros([Ns, int(adc.num_samples), 3]) extra_delay_time = 0 # Construct sequence # Initiate sequence seq = Sequence(system) u = 0 # For each direction for phi in phis: for theta in thetas: # Construct oblique gradient ug = get_3d_unit_grad(theta, phi) g_pre_x, g_pre_y, g_pre_z = make_oblique_gradients(gradient=g_pre, unit_grad=-1 * ug) g_ro_x, g_ro_y, g_ro_z = make_oblique_gradients(gradient=g_ro, unit_grad=ug) # Add components seq.add_block(rf, g_pre_x, g_pre_y, g_pre_z) seq.add_block(make_delay(extra_delay_time)) seq.add_block(g_ro_x, g_ro_y, g_ro_z, adc) seq.add_block(delayTR) # Store trajectory gpxh, gpyh, gpzh = make_oblique_gradients(gradient=g_pre, unit_grad=-0.5 * ug) ktraj[u, :, :] = get_ktraj_3d_rew_delay(g_ro_x, gpxh, g_ro_y, gpyh, g_ro_z, gpzh, adc) u += 1 # Display sequence plot #seq.plot(time_range=[-TR/2,1.5*TR]) # Test sequence validity #out_text = seq.test_report() #print(out_text) # Save sequence if save_seq: seq.write( f'seqs/code{enc_type}_{rf_type}_TR{TR*1e3:.0f}_TE{TE*1e3:.2f}_FA{flip}_N{N}_delay{extra_delay_time*1e3}ms.seq' ) savemat( f'seqs/ktraj_code{enc_type}_{rf_type}_TR{TR*1e3:.0f}_TE{TE*1e3:.2f}_FA{flip}_N{N}.mat', {'ktraj': ktraj}) return seq, TE, ktraj
def write_UTE_3D_rf_spoiled(N=64, FOV=250e-3, slab_thk=250e-3, FA=10, TR=10e-3, ro_asymmetry=0.97, os_factor=1, rf_type='sinc', rf_dur=1e-3, use_half_pulse=True, save_seq=True): """ Parameters ---------- N : int, default=64 Matrix size FOV : float, default=0.25 Field-of-view in [meters] slab_thk : float, default=0.25 Slab thickness in [meters] FA : float, default=10 Flip angle in [degrees] TR : float, default=0.01 Repetition time in [seconds] ro_asymmetry : float, default=0.97 The ratio A/B where a A/(A+B) portion of 2*Kmax is omitted and B/(A+B) is acquired. os_factor : float, default=1 Oversampling factor in readout The number of readout samples is the nearest integer from os_factor*N rf_type : str, default='sinc' RF pulse shape - 'sinc', 'gauss', or 'rect' rf_dur : float, default=0.001 RF pulse duration use_half_pulse : bool, default=True Whether to use half pulse excitation for shorter TE; This doubles both the number of excitations and acquisition time save_seq : bool, default=True Whether to save this sequence as a .seq file Returns ------- seq : Sequence PyPulseq 2D UTE sequence object TE : float Echo time of generated sequence in [seconds] ktraj : np.ndarray 3D k-space trajectory [spoke, readout sample, 3] where the last dimension refers to spatial frequency coordinates (kx, ky, kz) """ # Adapted from pypulseq demo write_ute.py (obtained mid-2021) system = Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6, adc_dead_time=20e-6) seq = Sequence(system=system) # Derived parameters dx = FOV / N Ns, Ntheta, Nphi = get_radk_params_3D(dx, FOV) # Make this function print(f'Using {Ns} spokes, with {Ntheta} thetas and {Nphi} phis') # Spoke angles thetas = np.linspace(0, np.pi, Ntheta, endpoint=False) phis = np.linspace(0, 2 * np.pi, Nphi, endpoint=False) ro_duration = 2.5e-3 ro_os = os_factor # Oversampling factor rf_spoiling_inc = 117 # RF spoiling increment value. if use_half_pulse: cp = 1 else: cp = 0.5 tbw = 2 # Sequence components if rf_type == 'sinc': rf, gz, gz_reph = make_sinc_pulse(flip_angle=FA * np.pi / 180, duration=rf_dur, slice_thickness=slab_thk, apodization=0.5, time_bw_product=tbw, center_pos=cp, system=system, return_gz=True) gz_ramp_reph = make_trapezoid(channel='z', area=-gz.fall_time * gz.amplitude / 2, system=system) elif rf_type == 'rect': rf = make_block_pulse(flip_angle=FA * np.pi / 180, duration=rf_dur, slice_thickness=slab_thk, return_gz=False) elif rf_type == 'gauss': rf, gz, gz_reph = make_gauss_pulse(flip_angle=FA * np.pi / 180, duration=rf_dur, slice_thickness=slab_thk, system=system, return_gz=True) gz_ramp_reph = make_trapezoid(channel='z', area=-gz.fall_time * gz.amplitude / 2, system=system) # Asymmetry! (0 - fully rewound; 1 - hall-echo) Nro = np.round(ro_os * N) # Number of readout points s = np.round(ro_asymmetry * Nro / 2) / (Nro / 2) dk = (1 / FOV) / (1 + s) ro_area = N * dk gro = make_trapezoid(channel='x', flat_area=ro_area, flat_time=ro_duration, system=system) adc = make_adc(num_samples=Nro, duration=gro.flat_time, delay=gro.rise_time, system=system) gro_pre = make_trapezoid(channel='x', area=-(gro.area - ro_area) / 2 - (ro_area / 2) * (1 - s), system=system) # Spoilers gro_spoil = make_trapezoid(channel='x', area=0.2 * N * dk, system=system) # Calculate timing TE = gro.rise_time + adc.dwell * Nro / 2 * (1 - s) if rf_type == 'sinc' or rf_type == 'gauss': if use_half_pulse: TE += gz.fall_time + calc_duration(gro_pre) else: TE += calc_duration(gz) / 2 + calc_duration(gro_pre) delay_TR = np.ceil( (TR - calc_duration(gro_pre) - calc_duration(gz) - calc_duration(gro)) / seq.grad_raster_time) * seq.grad_raster_time elif rf_type == 'rect': TE += calc_duration(gro_pre) delay_TR = np.ceil((TR - calc_duration(gro_pre) - calc_duration(gro)) / seq.grad_raster_time) * seq.grad_raster_time assert np.all(delay_TR >= calc_duration(gro_spoil) ) # The TR delay starts at the same time as the spoilers! print(f'TE = {TE * 1e6:.0f} us') C = int(use_half_pulse) + 1 # Starting RF phase and increments rf_phase = 0 rf_inc = 0 Nline = Ntheta * Nphi ktraj = np.zeros([Nline, int(adc.num_samples), 3]) u = 0 for th in range(Ntheta): for ph in range(Nphi): unit_grad = np.zeros(3) unit_grad[0] = np.sin(thetas[th]) * np.cos(phis[ph]) unit_grad[1] = np.sin(thetas[th]) * np.sin(phis[ph]) unit_grad[2] = np.cos(thetas[th]) # Two repeats if using half pulse for c in range(C): # RF spoiling rf.phase_offset = (rf_phase / 180) * np.pi adc.phase_offset = (rf_phase / 180) * np.pi rf_inc = np.mod(rf_inc + rf_spoiling_inc, 360.0) rf_phase = np.mod(rf_phase + rf_inc, 360.0) # Rewinder and readout gradients, vectorized gpx, gpy, gpz = make_oblique_gradients(gro_pre, unit_grad) grx, gry, grz = make_oblique_gradients(gro, unit_grad) gsx, gsy, gsz = make_oblique_gradients(gro_spoil, unit_grad) if rf_type == 'sinc' or rf_type == 'gauss': if use_half_pulse: # Reverse slice select amplitude (always z = 0) modify_gradient(gz, scale=-1) modify_gradient(gz_ramp_reph, scale=-1) gpz_reph = copy.deepcopy(gpz) modify_gradient(gpz_reph, scale=(gpz.area + gz_ramp_reph.area) / gpz.area) else: gpz_reph = copy.deepcopy(gpz) modify_gradient(gpz_reph, scale=(gpz.area + gz_reph.area) / gpz.area) seq.add_block(rf, gz) seq.add_block(gpx, gpy, gpz_reph) elif rf_type == 'rect': seq.add_block(rf) seq.add_block(gpx, gpy, gpz) seq.add_block(grx, gry, grz, adc) seq.add_block(gsx, gsy, gsz, make_delay(delay_TR)) #print(f'Spokes: {u+1}/{Nline}') ktraj[u, :, :] = get_ktraj_3d(grx, gry, grz, adc, [gpx], [gpy], [gpz]) u += 1 ok, error_report = seq.check_timing( ) # Check whether the timing of the sequence is correct if ok: print('Timing check passed successfully') else: print('Timing check failed. Error listing follows:') [print(e) for e in error_report] if save_seq: seq.write( f'ute_3d_rf-{rf_type}_rw_s{s}_N{N}_FOV{FOV}_TR{TR}_TE{TE}_C={use_half_pulse+1}.seq' ) savemat( f'ktraj_ute_3d_rw_s{s}_N{N}_FOV{FOV}_TR{TR}_TE{TE}_C={use_half_pulse+1}.mat', {'ktraj': ktraj}) return seq, TE, ktraj
def write_irse_interleaved_split_gradient(n=256, fov=250e-3, thk=5e-3, fa=90, te=12e-3, tr=2000e-3, ti=150e-3, slice_locations=[0], enc='xyz'): """ 2D IRSE sequence with overlapping gradient ramps and interleaved slices Inputs ------ n : integer Matrix size (isotropic) fov : float Field-of-View in [meters] thk : float Slice thickness in [meters] fa : float Flip angle in [degrees] te : float Echo Time in [seconds] tr : float Repetition Time in [seconds] ti : float Inversion Time in [seconds] slice_locations : array_like Array of slice locations from isocenter in [meters] enc : str Spatial encoding directions; 1st - readout; 2nd - phase encoding; 3rd - slice select Use str with any permutation of x, y, and z to obtain orthogonal slices e.g. The default 'xyz' means axial(z) slice with readout in x and phase encoding in y Returns ------- seq : pypulseq.Sequence.sequence Sequence object Output sequence object. Can be saved with seq.write('file_name.seq') sl_order : numpy.ndarray Randomly generated slice order. Useful for reconstruction. """ # ========= # SYSTEM LIMITS # ========= # Set the hardware limits and initialize sequence object dG = 250e-6 # Fixed ramp time for all gradients system = Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=100e-6, rf_dead_time=100e-6, adc_dead_time=10e-6) seq = Sequence(system) # ========= # TIME CALCULATIONS # ========= readout_time = 6.4e-3 + 2 * system.adc_dead_time t_ex = 2.5e-3 t_exwd = t_ex + system.rf_ringdown_time + system.rf_dead_time t_ref = 2e-3 t_refwd = t_ref + system.rf_ringdown_time + system.rf_dead_time t_sp = 0.5 * (te - readout_time - t_refwd) t_spex = 0.5 * (te - t_exwd - t_refwd) fsp_r = 1 fsp_s = 0.5 # ========= # ENCODING DIRECTIONS # ========== ch_ro = enc[0] ch_pe = enc[1] ch_ss = enc[2] # ========= # RF AND GRADIENT SHAPES - BASED ON RESOLUTION REQUIREMENTS : kmax and Npe # ========= # RF Phases rf_ex_phase = np.pi / 2 rf_ref_phase = 0 # Excitation phase (90 deg) flip_ex = 90 * np.pi / 180 rf_ex, gz, _ = make_sinc_pulse(flip_angle=flip_ex, system=system, duration=t_ex, slice_thickness=thk, apodization=0.5, time_bw_product=4, phase_offset=rf_ex_phase, return_gz=True) gs_ex = make_trapezoid(channel=ch_ss, system=system, amplitude=gz.amplitude, flat_time=t_exwd, rise_time=dG) # Refocusing (same gradient & RF is used for initial inversion) flip_ref = fa * np.pi / 180 rf_ref, gz, _ = make_sinc_pulse(flip_angle=flip_ref, system=system, duration=t_ref, slice_thickness=thk, apodization=0.5, time_bw_product=4, phase_offset=rf_ref_phase, use='refocusing', return_gz=True) gs_ref = make_trapezoid(channel=ch_ss, system=system, amplitude=gs_ex.amplitude, flat_time=t_refwd, rise_time=dG) ags_ex = gs_ex.area / 2 gs_spr = make_trapezoid(channel=ch_ss, system=system, area=ags_ex * (1 + fsp_s), duration=t_sp, rise_time=dG) gs_spex = make_trapezoid(channel=ch_ss, system=system, area=ags_ex * fsp_s, duration=t_spex, rise_time=dG) delta_k = 1 / fov k_width = n * delta_k gr_acq = make_trapezoid(channel=ch_ro, system=system, flat_area=k_width, flat_time=readout_time, rise_time=dG) adc = make_adc(num_samples=n, duration=gr_acq.flat_time - 40e-6, delay=20e-6) gr_spr = make_trapezoid(channel=ch_ro, system=system, area=gr_acq.area * fsp_r, duration=t_sp, rise_time=dG) gr_spex = make_trapezoid(channel=ch_ro, system=system, area=gr_acq.area * (1 + fsp_r), duration=t_spex, rise_time=dG) agr_spr = gr_spr.area agr_preph = gr_acq.area / 2 + agr_spr gr_preph = make_trapezoid(channel=ch_ro, system=system, area=agr_preph, duration=t_spex, rise_time=dG) phase_areas = (np.arange(n) - n / 2) * delta_k # Split gradients and recombine into blocks gs1_times = [0, gs_ex.rise_time] gs1_amp = [0, gs_ex.amplitude] gs1 = make_extended_trapezoid(channel=ch_ss, times=gs1_times, amplitudes=gs1_amp) gs2_times = [0, gs_ex.flat_time] gs2_amp = [gs_ex.amplitude, gs_ex.amplitude] gs2 = make_extended_trapezoid(channel=ch_ss, times=gs2_times, amplitudes=gs2_amp) gs3_times = [ 0, gs_spex.rise_time, gs_spex.rise_time + gs_spex.flat_time, gs_spex.rise_time + gs_spex.flat_time + gs_spex.fall_time ] gs3_amp = [ gs_ex.amplitude, gs_spex.amplitude, gs_spex.amplitude, gs_ref.amplitude ] gs3 = make_extended_trapezoid(channel=ch_ss, times=gs3_times, amplitudes=gs3_amp) gs4_times = [0, gs_ref.flat_time] gs4_amp = [gs_ref.amplitude, gs_ref.amplitude] gs4 = make_extended_trapezoid(channel=ch_ss, times=gs4_times, amplitudes=gs4_amp) gs5_times = [ 0, gs_spr.rise_time, gs_spr.rise_time + gs_spr.flat_time, gs_spr.rise_time + gs_spr.flat_time + gs_spr.fall_time ] gs5_amp = [gs_ref.amplitude, gs_spr.amplitude, gs_spr.amplitude, 0] gs5 = make_extended_trapezoid(channel=ch_ss, times=gs5_times, amplitudes=gs5_amp) gs7_times = [ 0, gs_spr.rise_time, gs_spr.rise_time + gs_spr.flat_time, gs_spr.rise_time + gs_spr.flat_time + gs_spr.fall_time ] gs7_amp = [0, gs_spr.amplitude, gs_spr.amplitude, gs_ref.amplitude] gs7 = make_extended_trapezoid(channel=ch_ss, times=gs7_times, amplitudes=gs7_amp) gr3 = gr_preph gr5_times = [ 0, gr_spr.rise_time, gr_spr.rise_time + gr_spr.flat_time, gr_spr.rise_time + gr_spr.flat_time + gr_spr.fall_time ] gr5_amp = [0, gr_spr.amplitude, gr_spr.amplitude, gr_acq.amplitude] gr5 = make_extended_trapezoid(channel=ch_ro, times=gr5_times, amplitudes=gr5_amp) gr6_times = [0, readout_time] gr6_amp = [gr_acq.amplitude, gr_acq.amplitude] gr6 = make_extended_trapezoid(channel=ch_ro, times=gr6_times, amplitudes=gr6_amp) gr7_times = [ 0, gr_spr.rise_time, gr_spr.rise_time + gr_spr.flat_time, gr_spr.rise_time + gr_spr.flat_time + gr_spr.fall_time ] gr7_amp = [gr_acq.amplitude, gr_spr.amplitude, gr_spr.amplitude, 0] gr7 = make_extended_trapezoid(channel=ch_ro, times=gr7_times, amplitudes=gr7_amp) t_ex = gs1.t[-1] + gs2.t[-1] + gs3.t[-1] t_ref = gs4.t[-1] + gs5.t[-1] + gs7.t[-1] + readout_time t_end = gs4.t[-1] + gs5.t[-1] # Calculate maximum number of slices that can fit in one TR # Without spoilers on each side TE_prime = 0.5 * calc_duration(gs_ref) + ti + te + 0.5 * readout_time + np.max( [calc_duration(gs7), calc_duration(gr7)]) + \ calc_duration(gs4) + calc_duration(gs5) ns_per_TR = np.floor(tr / TE_prime) print('Number of slices that can be accommodated = ' + str(ns_per_TR)) # Lengthen TR to accommodate slices if needed, and display message n_slices = len(slice_locations) if (ns_per_TR < n_slices): print( f'TR too short, adapted to include all slices to: {n_slices * TE_prime + 50e-6} s' ) TR = round(n_slices * TE_prime + 50e-6, ndigits=5) print('New TR = ' + str(TR)) ns_per_TR = np.floor(TR / TE_prime) if (n_slices < ns_per_TR): ns_per_TR = n_slices # randperm so that adjacent slices do not get excited one after the other sl_order = np.random.permutation(n_slices) print('Number of slices acquired per TR = ' + str(ns_per_TR)) # Delays TI_fill = ti - (0.5 * calc_duration(gs_ref) + calc_duration(gs1) + 0.5 * calc_duration(gs2)) delay_TI = make_delay(TI_fill) TR_fill = tr - ns_per_TR * TE_prime delay_TR = make_delay(TR_fill) for k_ex in range(n): phase_area = phase_areas[k_ex] gp_pre = make_trapezoid(channel=ch_pe, system=system, area=phase_area, duration=t_sp, rise_time=dG) gp_rew = make_trapezoid(channel=ch_pe, system=system, area=-phase_area, duration=t_sp, rise_time=dG) s_in_TR = 0 for s in range(len(sl_order)): # rf_ex.freq_offset = gs_ex.amplitude * slice_thickness * (sl_order[s] - (n_slices - 1) / 2) # rf_ref.freq_offset = gs_ref.amplitude * slice_thickness * (sl_order[s] - (n_slices - 1) / 2) rf_ex.freq_offset = gs_ex.amplitude * slice_locations[sl_order[s]] rf_ref.freq_offset = gs_ref.amplitude * slice_locations[ sl_order[s]] rf_ex.phase_offset = rf_ex_phase - 2 * np.pi * rf_ex.freq_offset * calc_rf_center( rf_ex)[0] rf_ref.phase_offset = rf_ref_phase - 2 * np.pi * rf_ref.freq_offset * calc_rf_center( rf_ref)[0] # Inversion using refocusing pulse seq.add_block(gs_ref, rf_ref) seq.add_block(delay_TI) # SE portion seq.add_block(gs1) seq.add_block(gs2, rf_ex) seq.add_block(gs3, gr3) seq.add_block(gs4, rf_ref) seq.add_block(gs5, gr5, gp_pre) seq.add_block(gr6, adc) seq.add_block(gs7, gr7, gp_rew) seq.add_block(gs4) seq.add_block(gs5) s_in_TR += 1 if (s_in_TR == ns_per_TR): seq.add_block(delay_TR) s_in_TR = 0 # Check timing to make sure sequence runs on scanner seq.check_timing() return seq, sl_order
def write_tse(n=256, fov=250e-3, thk=5e-3, fa_exc=90, fa_ref=180, te=50e-3, tr=2000e-3, slice_locations=[0], turbo_factor=4, enc='xyz'): """ 2D TSE sequence with interleaved slices and user-defined turbo factor Inputs ------ n : integer Matrix size (isotropic) fov : float Field-of-View in [meters] thk : float Slice thickness in [meters] fa_exc : float Initial excitation flip angle in [degrees] fa_ref : float All following flip angles for spin echo refocusing in [degrees] te : float Echo Time in [seconds] tr : float Repetition Time in [seconds] slice_locations : array_like Array of slice locations from isocenter in [meters] turbo_factor : integer Number of echoes per TR enc : str Spatial encoding directions; 1st - readout; 2nd - phase encoding; 3rd - slice select Use str with any permutation of x, y, and z to obtain orthogonal slices e.g. The default 'xyz' means axial(z) slice with readout in x and phase encoding in y Returns ------- seq : pypulseq.Sequence.sequence Sequence object Output sequence object. Can be saved with seq.write('file_name.seq') pe_order : numpy.ndarray (turbo_factor) x (number_of_excitations) matrix of phase encoding order This is required for phase sorting before ifft2 reconstruction. """ # Set system limits ramp_time = 250e-6 # Ramp up/down time for all gradients where this is specified system = Opts( max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=100e-6, # changed from 30e-6 rf_dead_time=100e-6, adc_dead_time=20e-6) # Initialize sequence seq = Sequence(system) # Spatial encoding directions ch_ro = enc[0] ch_pe = enc[1] ch_ss = enc[2] # Derived parameters Nf, Np = (n, n) delta_k = 1 / fov k_width = Nf * delta_k # Number of echoes per excitation (i.e. turbo factor) n_echo = turbo_factor # Readout duration readout_time = 6.4e-3 + 2 * system.adc_dead_time # Excitation pulse duration t_ex = 2.5e-3 t_exwd = t_ex + system.rf_ringdown_time + system.rf_dead_time # Refocusing pulse duration t_ref = 2e-3 t_refwd = t_ref + system.rf_ringdown_time + system.rf_dead_time # Time gaps for spoilers t_sp = 0.5 * (te - readout_time - t_refwd ) # time gap between pi-pulse and readout t_spex = 0.5 * (te - t_exwd - t_refwd ) # time gap between pi/2-pulse and pi-pulse # Spoiling factors fsp_r = 1 # in readout direction per refocusing fsp_s = 0.5 # in slice direction per refocusing # 3. Calculate sequence components ## Slice-selective RF pulses & gradient #* RF pulses with zero frequency shift are created. The frequency is then changed before adding the pulses to sequence blocks for each slice. # 90 deg pulse (+y') rf_ex_phase = np.pi / 2 flip_ex = fa_exc * np.pi / 180 rf_ex, g_ss, _ = make_sinc_pulse(flip_angle=flip_ex, system=system, duration=t_ex, slice_thickness=thk, apodization=0.5, time_bw_product=4, phase_offset=rf_ex_phase, return_gz=True) gs_ex = make_trapezoid(channel=ch_ss, system=system, amplitude=g_ss.amplitude, flat_time=t_exwd, rise_time=ramp_time) # 180 deg pulse (+x') rf_ref_phase = 0 flip_ref = fa_ref * np.pi / 180 rf_ref, gz, _ = make_sinc_pulse(flip_angle=flip_ref, system=system, duration=t_ref, slice_thickness=thk, apodization=0.5, time_bw_product=4, phase_offset=rf_ref_phase, use='refocusing', return_gz=True) gs_ref = make_trapezoid(channel=ch_ss, system=system, amplitude=gs_ex.amplitude, flat_time=t_refwd, rise_time=ramp_time) rf_ex, g_ss, _ = make_sinc_pulse(flip_angle=flip_ex, system=system, duration=t_ex, slice_thickness=thk, apodization=0.5, time_bw_product=4, phase_offset=rf_ex_phase, return_gz=True) ## Make gradients and ADC # gs_spex : slice direction spoiler between initial excitation and 1st 180 pulse # gs_spr : slice direction spoiler between 180 pulses # gr_spr : readout direction spoiler; area is (fsp_r) x (full readout area) # SS spoiling ags_ex = gs_ex.area / 2 gs_spr = make_trapezoid(channel=ch_ss, system=system, area=ags_ex * (1 + fsp_s), duration=t_sp, rise_time=ramp_time) gs_spex = make_trapezoid(channel=ch_ss, system=system, area=ags_ex * fsp_s, duration=t_spex, rise_time=ramp_time) # Readout gradient and ADC gr_acq = make_trapezoid(channel=ch_ro, system=system, flat_area=k_width, flat_time=readout_time, rise_time=ramp_time) # No need for risetime delay since it is set at beginning of flattime; delay is ADC deadtime adc = make_adc(num_samples=Nf, duration=gr_acq.flat_time - 40e-6, delay=20e-6) # RO spoiling gr_spr = make_trapezoid(channel=ch_ro, system=system, area=gr_acq.area * fsp_r, duration=t_sp, rise_time=ramp_time) # Following is not used anywhere # gr_spex = make_trapezoid(channel=ch_ro, system=system, area=gr_acq.area * (1 + fsp_r), duration=t_spex, rise_time=ramp_time) # Prephasing gradient in RO direction agr_preph = gr_acq.area / 2 + gr_spr.area gr_preph = make_trapezoid(channel=ch_ro, system=system, area=agr_preph, duration=t_spex, rise_time=ramp_time) # Phase encoding areas # Need to export the pe_order for reconsturuction # Number of readouts/echoes to be produced per TR n_ex = math.floor(Np / n_echo) pe_steps = np.arange(1, n_echo * n_ex + 1) - 0.5 * n_echo * n_ex - 1 if divmod(n_echo, 2)[1] == 0: # If there is an even number of echoes pe_steps = np.roll(pe_steps, -round(n_ex / 2)) pe_order = pe_steps.reshape((n_ex, n_echo), order='F').T savemat('pe_info.mat', {'order': pe_order, 'dims': ['n_echo', 'n_ex']}) phase_areas = pe_order * delta_k # Split gradients and recombine into blocks # gs1 : ramp up of gs_ex gs1_times = [0, gs_ex.rise_time] gs1_amp = [0, gs_ex.amplitude] gs1 = make_extended_trapezoid(channel=ch_ss, times=gs1_times, amplitudes=gs1_amp) # gs2 : flat part of gs_ex gs2_times = [0, gs_ex.flat_time] gs2_amp = [gs_ex.amplitude, gs_ex.amplitude] gs2 = make_extended_trapezoid(channel=ch_ss, times=gs2_times, amplitudes=gs2_amp) # gs3 : Bridged slice pre-spoiler gs3_times = [ 0, gs_spex.rise_time, gs_spex.rise_time + gs_spex.flat_time, gs_spex.rise_time + gs_spex.flat_time + gs_spex.fall_time ] gs3_amp = [ gs_ex.amplitude, gs_spex.amplitude, gs_spex.amplitude, gs_ref.amplitude ] gs3 = make_extended_trapezoid(channel=ch_ss, times=gs3_times, amplitudes=gs3_amp) # gs4 : Flat slice selector for pi-pulse gs4_times = [0, gs_ref.flat_time] gs4_amp = [gs_ref.amplitude, gs_ref.amplitude] gs4 = make_extended_trapezoid(channel=ch_ss, times=gs4_times, amplitudes=gs4_amp) # gs5 : Bridged slice post-spoiler gs5_times = [ 0, gs_spr.rise_time, gs_spr.rise_time + gs_spr.flat_time, gs_spr.rise_time + gs_spr.flat_time + gs_spr.fall_time ] gs5_amp = [gs_ref.amplitude, gs_spr.amplitude, gs_spr.amplitude, 0] gs5 = make_extended_trapezoid(channel=ch_ss, times=gs5_times, amplitudes=gs5_amp) # gs7 : The gs3 for next pi-pulse gs7_times = [ 0, gs_spr.rise_time, gs_spr.rise_time + gs_spr.flat_time, gs_spr.rise_time + gs_spr.flat_time + gs_spr.fall_time ] gs7_amp = [0, gs_spr.amplitude, gs_spr.amplitude, gs_ref.amplitude] gs7 = make_extended_trapezoid(channel=ch_ss, times=gs7_times, amplitudes=gs7_amp) # gr3 : pre-readout gradient gr3 = gr_preph # gr5 : Readout post-spoiler gr5_times = [ 0, gr_spr.rise_time, gr_spr.rise_time + gr_spr.flat_time, gr_spr.rise_time + gr_spr.flat_time + gr_spr.fall_time ] gr5_amp = [0, gr_spr.amplitude, gr_spr.amplitude, gr_acq.amplitude] gr5 = make_extended_trapezoid(channel=ch_ro, times=gr5_times, amplitudes=gr5_amp) # gr6 : Flat readout gradient gr6_times = [0, readout_time] gr6_amp = [gr_acq.amplitude, gr_acq.amplitude] gr6 = make_extended_trapezoid(channel=ch_ro, times=gr6_times, amplitudes=gr6_amp) # gr7 : the gr3 for next repeat gr7_times = [ 0, gr_spr.rise_time, gr_spr.rise_time + gr_spr.flat_time, gr_spr.rise_time + gr_spr.flat_time + gr_spr.fall_time ] gr7_amp = [gr_acq.amplitude, gr_spr.amplitude, gr_spr.amplitude, 0] gr7 = make_extended_trapezoid(channel=ch_ro, times=gr7_times, amplitudes=gr7_amp) # Timing (delay) calculations # delay_TR : delay at the end of each TSE pulse train (i.e. each TR) t_ex = gs1.t[-1] + gs2.t[-1] + gs3.t[-1] t_ref = gs4.t[-1] + gs5.t[-1] + gs7.t[-1] + readout_time t_end = gs4.t[-1] + gs5.t[-1] TE_train = t_ex + n_echo * t_ref + t_end TR_fill = (tr - n_slices * TE_train) / n_slices TR_fill = system.grad_raster_time * round( TR_fill / system.grad_raster_time) if TR_fill < 0: TR_fill = 1e-3 print( f'TR too short, adapted to include all slices to: {1000 * n_slices * (TE_train + TR_fill)} ms' ) else: print(f'TR fill: {1000 * TR_fill} ms') delay_TR = make_delay(TR_fill) # Add building blocks to sequence for k_ex in range(n_ex + 1): # For each TR for s in range(n_slices): # For each slice (multislice) if slice_locations is None: rf_ex.freq_offset = gs_ex.amplitude * thk * ( s - (n_slices - 1) / 2) rf_ref.freq_offset = gs_ref.amplitude * thk * ( s - (n_slices - 1) / 2) rf_ex.phase_offset = rf_ex_phase - 2 * np.pi * rf_ex.freq_offset * calc_rf_center( rf_ex)[0] rf_ref.phase_offset = rf_ref_phase - 2 * np.pi * rf_ref.freq_offset * calc_rf_center( rf_ref)[0] else: rf_ex.freq_offset = gs_ex.amplitude * slice_locations[s] rf_ref.freq_offset = gs_ref.amplitude * slice_locations[s] rf_ex.phase_offset = rf_ex_phase - 2 * np.pi * rf_ex.freq_offset * calc_rf_center( rf_ex)[0] rf_ref.phase_offset = rf_ref_phase - 2 * np.pi * rf_ref.freq_offset * calc_rf_center( rf_ref)[0] seq.add_block(gs1) seq.add_block(gs2, rf_ex) # make sure gs2 has channel ch_ss seq.add_block(gs3, gr3) for k_echo in range(n_echo): # For each echo if k_ex > 0: phase_area = phase_areas[k_echo, k_ex - 1] else: # First TR is skipped so zero phase encoding is needed phase_area = 0.0 # 0.0 and not 0 because -phase_area should successfully result in negative zero gp_pre = make_trapezoid(channel=ch_pe, system=system, area=phase_area, duration=t_sp, rise_time=ramp_time) # print('gp_pre info: ', gp_pre) gp_rew = make_trapezoid(channel=ch_pe, system=system, area=-phase_area, duration=t_sp, rise_time=ramp_time) seq.add_block(gs4, rf_ref) seq.add_block(gs5, gr5, gp_pre) # Skipping first TR if k_ex > 0: seq.add_block(gr6, adc) else: seq.add_block(gr6) seq.add_block(gs7, gr7, gp_rew) seq.add_block(gs4) seq.add_block(gs5) seq.add_block(delay_TR) # Check timing to make sure sequence runs on scanner seq.check_timing() return seq, pe_order
def tbt_seq2xml(n): seq = Sequence() seq_path = f'sim/ismrm_abstract/spgr_var_N/spgr_gspoil_N{n}_Ns1_TE10ms_TR50ms_FA30deg_acq_112020.seq' seq.read(seq_path) seq2xml(seq, seq_name=f'spgr{n}',out_folder=f'sim/ismrm_abstract/spgr_var_N/spgr{n}')
""" import math import matplotlib.pyplot as plt import numpy as np from pypulseq.Sequence.sequence import Sequence from pypulseq.make_adc import make_adc from pypulseq.make_sinc_pulse import make_sinc_pulse from pypulseq.make_trap_pulse import make_trapezoid from pypulseq.opts import Opts # ====== # SETUP # ====== seq = Sequence() # Create a new sequence object # Define FOV and resolution fov = 220e-3 Nx = 64 Ny = 64 slice_thickness = 3e-3 # Slice thickness n_slices = 3 # Set system limits system = Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6) # ====== # CREATE EVENTS # ====== # Create 90 degree slice selection pulse and gradient
def simulate_pulseq_jemris( seq_path, phantom_info, coil_fov, tx='uniform', rx='uniform', # TODO add input that includes sequence info for # TODO dimensioning the RO points into kspace tx_maps=None, rx_maps=None, sim_name=None, env_option="local"): """Runs simulation using an already-made .seq file Inputs ------ seq_path : str Path to seq file phantom_info : dict Information used to create phantom; input to create_and_save_phantom() coil_fov : float Field-of-view of coil in mm tx : str, optional Type of tx coil; default is 'uniform'; the only other option is 'custom' rx : str, optional Type of rx coil; default is 'uniform'; the only other option is 'custom' tx_maps : list, optional List of np.ndarray (dtype='complex') maps for all tx channels Required for 'custom' type tx rx_maps : list, optional List of np.ndarray (dtype='complex') maps for all rx channels Required for 'custom' type rx sim_name : str, optional Used as folder name inside sim folder Default is None, in which case sim_name will be set to the current timestamp Returns ------- sim_output : Delivers output from sim_jemris """ if sim_name is None: sim_name = time.strftime("%Y%m%d%H%M%S") if env_option == 'local': target_path = 'sim\\' + sim_name elif env_option == 'colab': target_path = 'sim\\' + sim_name # Make target folder dir_str = f'sim\\{sim_name}' if not os.path.isdir(dir_str): os.system(f'mkdir {dir_str}') # Convert .seq to .xml seq = Sequence() seq.read(seq_path) print(seq.get_block(1)) seq_name = seq_path[seq_path.rfind('/') + 1:seq_path.rfind('.seq')] seq2xml(seq, seq_name=seq_name, out_folder=str(target_path)) # Make phantom and save as .h5 file pht_name = create_and_save_phantom(phantom_info, out_folder=target_path) # Make sure we have the tx/rx files tx_filename = tx + '.xml' rx_filename = rx + '.xml' # Save Tx as xml if tx == 'uniform': cp_command = f'copy {os.getcwd()}\\sim\\{tx}.xml {os.getcwd()}\\{str(target_path)}\\{tx}.xml' print(cp_command) a = os.system(cp_command) print(a) elif tx == 'custom' and tx_maps is not None: coil2xml(b1maps=tx_maps, fov=coil_fov, name='custom_tx', out_folder=target_path) tx_filename = 'custom_tx.xml' else: raise ValueError('Tx coil type not found') # save Rx as xml if rx == 'uniform': os.system(f'copy sim\\{rx}.xml sim\\{str(target_path)}') elif rx == 'custom' and rx_maps is not None: coil2xml(b1maps=rx_maps, fov=coil_fov, name='custom_rx', out_folder=target_path) rx_filename = 'custom_rx.xml' else: raise ValueError('Rx coil type not found') # Run simuluation in target folder list_sim_files = { 'seq_xml': seq_name + '.xml', 'pht_h5': pht_name + '.h5', 'tx_xml': tx_filename, 'rx_xml': rx_filename } sim_output = sim_jemris(list_sim_files=list_sim_files, working_folder=target_path) return sim_output
def make_pulseq_gre_oblique(fov, n, thk, fa, tr, te, enc='xyz', slice_locs=None, write=False): """Makes a gradient-echo sequence in any plane 2D oblique multi-slice gradient-echo pulse sequence with Cartesian encoding Oblique means that each of slice-selection, phase encoding, and frequency encoding can point in any specified direction Parameters ---------- fov : array_like Isotropic field-of-view, or length-2 list [fov_readout, fov_phase], in meters n : array_like Isotropic matrix size, or length-2 list [n_readout, n_phase] thk : float Slice thickness in meters fa : float Flip angle in degrees tr : float Repetition time in seconds te : float Echo time in seconds enc : str or array_like, optional Spatial encoding directions 1st - readout; 2nd - phase encoding; 3rd - slice select - Use str with any permutation of x, y, and z to obtain orthogonal slices e.g. The default 'xyz' means axial(z) slice with readout in x and phase encoding in y - Use list to indicate vectors in the encoding directions for oblique slices They should be perpendicular to each other, but not necessarily unit vectors e.g. [(2,1,0),(-1,2,0),(0,0,1)] rotates the two in-plane encoding directions for an axial slice slice_locs : array_like, optional Slice locations from isocenter in meters Default is None which means a single slice at the center write : bool, optional Whether to write seq into file; default is False Returns ------- seq : Sequence Pulse sequence as a Pulseq object """ # System options # kwargs_for_opts = {"rf_ring_down_time": 0, "rf_dead_time": 0} kwargs_for_opts = {'max_grad': 32, 'grad_unit': 'mT/m', 'max_slew': 130, 'slew_unit': 'T/m/s', 'rf_ring_down_time': 30e-6, 'rf_dead_time': 100e-6, 'adc_dead_time': 20e-6} system = Opts(kwargs_for_opts) seq = Sequence(system) # Calculate unit gradients for ss, fe, pe ug_fe, ug_pe, ug_ss = parse_enc(enc) # Sequence parameters Nf, Np = (n, n) if isinstance(n, int) else (n[0], n[1]) delta_k_ro, delta_k_pe = (1 / fov, 1 / fov) if isinstance(fov, float) else (1 / fov[0], 1 / fov[1]) kWidth_ro = Nf * delta_k_ro flip = fa * pi / 180 # Slice select: RF and gradient kwargs_for_sinc = {"flip_angle": flip, "system": system, "duration": 4e-3, "slice_thickness": thk, "apodization": 0.5, "time_bw_product": 4} rf, g_ss = make_sinc_pulse(kwargs_for_sinc, 2) g_ss_x, g_ss_y, g_ss_z = make_oblique_gradients(g_ss, ug_ss) # Readout and ADC readoutTime = 6.4e-3 kwargs_for_g_ro = {"channel": 'x', "system": system, "flat_area": kWidth_ro, "flat_time": readoutTime} g_ro = make_trapezoid(kwargs_for_g_ro) g_ro_x, g_ro_y, g_ro_z = make_oblique_gradients(g_ro, ug_fe) # kwargs_for_adc = {"num_samples": Nf, "duration": g_ro.flat_time, "delay": g_ro.rise_time} adc = makeadc(kwargs_for_adc) # Readout rewinder kwargs_for_g_ro_pre = {"channel": 'x', "system": system, "area": -g_ro.area / 2, "duration": 2e-3} g_ro_pre = make_trapezoid(kwargs_for_g_ro_pre) g_ro_pre_x, g_ro_pre_y, g_ro_pre_z = make_oblique_gradients(g_ro_pre, ug_fe) # # Slice refocusing kwargs_for_g_ss_reph = {"channel": 'z', "system": system, "area": -g_ss.area / 2, "duration": 2e-3} g_ss_reph = make_trapezoid(kwargs_for_g_ss_reph) g_ss_reph_x, g_ss_reph_y, g_ss_reph_z = make_oblique_gradients(g_ss_reph, ug_ss) # Prepare phase areas phase_areas = (np.arange(Np) - (Np / 2)) * delta_k_pe TE, TR = te, tr delayTE = TE - calc_duration(g_ro_pre) - calc_duration(g_ss) / 2 - calc_duration(g_ro) / 2 delayTR = TR - calc_duration(g_ro_pre) - calc_duration(g_ss) - calc_duration(g_ro) - delayTE delay1 = make_delay(delayTE) delay2 = make_delay(delayTR) if slice_locs is None: locs = [0] else: locs = slice_locs # Construct sequence! for u in range(len(locs)): # add frequency offset rf.freq_offset = g_ss.amplitude * locs[u] for i in range(Np): seq.add_block(rf, g_ss_x, g_ss_y, g_ss_z) kwargs_for_g_pe = {"channel": 'y', "system": system, "area": phase_areas[i], "duration": 2e-3} g_pe = make_trapezoid(kwargs_for_g_pe) g_pe_x, g_pe_y, g_pe_z = make_oblique_gradients(g_pe, ug_pe) pre_grads_list = [g_ro_pre_x, g_ro_pre_y, g_ro_pre_z, g_ss_reph_x, g_ss_reph_y, g_ss_reph_z, g_pe_x, g_pe_y, g_pe_z] gtx, gty, gtz = combine_trap_grad_xyz(gradients=pre_grads_list, system=system, dur=2e-3) seq.add_block(gtx, gty, gtz) seq.add_block(delay1) seq.add_block(g_ro_x, g_ro_y, g_ro_z, adc) seq.add_block(delay2) if write: seq.write( "gre_fov{:.0f}mm_Nf{:d}_Np{:d}_TE{:.0f}ms_TR{:.0f}ms_FA{:.0f}deg.seq".format(fov * 1000, Nf, Np, TE * 1000, TR * 1000, flip * 180 / pi)) print('GRE sequence constructed') return seq
def make_pulseq_gre(fov, n, thk, fa, tr, te, enc='xyz', slice_locs=None, write=False): """Makes a gradient-echo sequence 2D orthogonal multi-slice gradient-echo pulse sequence with Cartesian encoding Orthogonal means that each of slice-selection, phase encoding, and frequency encoding aligns with the x, y, or z directions Parameters ---------- fov : float Field-of-view in meters (isotropic) n : int Matrix size (isotropic) thk : float Slice thickness in meters fa : float Flip angle in degrees tr : float Repetition time in seconds te : float Echo time in seconds enc : str, optional Spatial encoding directions 1st - readout; 2nd - phase encoding; 3rd - slice select Default 'xyz' means axial(z) slice with readout in x and phase encoding in y slice_locs : array_like, optional Slice locations from isocenter in meters Default is None which means a single slice at the center write : bool, optional Whether to write seq into file; default is False Returns ------- seq : Sequence Pulse sequence as a Pulseq object """ kwargs_for_opts = {"rf_ring_down_time": 0, "rf_dead_time": 0} system = Opts(kwargs_for_opts) seq = Sequence(system) Nf = n Np = n flip = fa * pi / 180 kwargs_for_sinc = {"flip_angle": flip, "system": system, "duration": 4e-3, "slice_thickness": thk, "apodization": 0.5, "time_bw_product": 4} rf, g_ss = make_sinc_pulse(kwargs_for_sinc, 2) g_ss.channel = enc[2] delta_k = 1 / fov kWidth = Nf * delta_k # Readout and ADC readoutTime = 6.4e-3 kwargs_for_g_ro = {"channel": enc[0], "system": system, "flat_area": kWidth, "flat_time": readoutTime} g_ro = make_trapezoid(kwargs_for_g_ro) kwargs_for_adc = {"num_samples": Nf, "duration": g_ro.flat_time, "delay": g_ro.rise_time} adc = makeadc(kwargs_for_adc) # Readout rewinder kwargs_for_g_ro_pre = {"channel": enc[0], "system": system, "area": -g_ro.area / 2, "duration": 2e-3} g_ro_pre = make_trapezoid(kwargs_for_g_ro_pre) # Slice refocusing kwargs_for_g_ss_reph = {"channel": enc[2], "system": system, "area": -g_ss.area / 2, "duration": 2e-3} g_ss_reph = make_trapezoid(kwargs_for_g_ss_reph) phase_areas = (np.arange(Np) - (Np / 2)) * delta_k # TE, TR = 10e-3, 1000e-3 TE, TR = te, tr delayTE = TE - calc_duration(g_ro_pre) - calc_duration(g_ss) / 2 - calc_duration(g_ro) / 2 delayTR = TR - calc_duration(g_ro_pre) - calc_duration(g_ss) - calc_duration(g_ro) - delayTE delay1 = make_delay(delayTE) delay2 = make_delay(delayTR) if slice_locs is None: locs = [0] else: locs = slice_locs for u in range(len(locs)): # add frequency offset rf.freq_offset = g_ss.amplitude * locs[u] for i in range(Np): seq.add_block(rf, g_ss) kwargs_for_g_pe = {"channel": enc[1], "system": system, "area": phase_areas[i], "duration": 2e-3} g_pe = make_trapezoid(kwargs_for_g_pe) seq.add_block(g_ro_pre, g_pe, g_ss_reph) seq.add_block(delay1) seq.add_block(g_ro, adc) seq.add_block(delay2) if write: seq.write( "gre_fov{:.0f}mm_Nf{:d}_Np{:d}_TE{:.0f}ms_TR{:.0f}ms_FA{:.0f}deg.seq".format(fov * 1000, Nf, Np, TE * 1000, TR * 1000, flip * 180 / pi)) print('GRE sequence constructed') return seq
meta_filename = meta_folder+seq_name+'.h5' if os.path.isfile(meta_filename): os.remove(meta_filename) meta_file = ismrmrd.Dataset(meta_filename) hdr = ismrmrd.xsd.ismrmrdHeader() t_min = TE + dwelltime/2 # save trajectory starting point for B0-correction params_hdr = {"fov": fov, "res": res, "slices": slices, "slice_res": slice_res, "nintl":int(Nintl/redfac), "avg": averages, "nsegments": num_segments, "dwelltime": dwelltime, "t_min": t_min, "trajtype": "spiral"} create_hdr(hdr, params_hdr) meta_file.write_xml_header(hdr.toXML('utf-8')) #%% Add sequence blocks to sequence & write acquisitions to metadata # Set up the sequence seq = Sequence() # Definitions section in seq file seq.set_definition("Name", seq_name) # metadata file name is saved in Siemens header for reco seq.set_definition("FOV", [1e-3*fov, 1e-3*fov, slice_res]) # for FOV positioning seq.set_definition("Slice_Thickness", "%f" % (slice_res*(1+dist_fac*1e-2)*(slices-1)+slice_res)) # we misuse this to show the total covered head area in the GUI if num_segments > 1: seq.set_definition("MaxAdcSegmentLength", "%d" % int(num_samples/num_segments+0.5)) # for automatic ADC segment length setting # Noise scans noise_samples = 256 noise_dwelltime = 2e-6 noise_adc = make_adc(system=system, num_samples=256, dwell=noise_dwelltime, delay=system.adc_dead_time) noise_delay = make_delay(d=ph.round_up_to_raster(calc_duration(noise_adc)+1e-3,decimals=5)) # add some more time to the ADC delay to be safe for k in range(noisescans): seq.add_block(noise_adc, noise_delay)
def write_gapped_swift(N, BW, R=128, TR=100e-3, flip=90, L_over=1, enc_type='3D', n_adc=2, spoke_os=1, fm_type='full', output=False): """ Parameters ---------- N : int Isotropic matrix size R : float Time-bandwidth product TR : float Repetition time [seconds] flip : float Flip angle [degrees] as calculated with equation ? in ? L_over : float, default=1 RF oversampling factor enc_type : str, default='3D' Type of sequence encoding: '2D' or '3D' spoke_os : int, default=1 Oversampling factor for theta fm_type : str, default='full' The way frequency modulation is handled 'none' - each pulse segment has a fixed phase 'linear' - each pulse segment has a fixed frequency corresponding to FM 'full' - each pulse segment has complete FM based on the ideal pulse output : str, default=False Whether the sequence is saved as a seq file Returns ------- seq : Sequence PyPulseq gapped SWIFT sequence object seq_info : dict Information for reconstruction 'thetas' - all polar angles 'phis' - all azimuthal angles 'rf_complex' - Complex RF waveform 'ktraj': 3D k-space trajectory """ # Let's do SWIFT # System system = Opts(max_grad=32, grad_unit='mT/m', max_slew=130, slew_unit='T/m/s', rf_ringdown_time=30e-6, rf_dead_time=100e-6, adc_dead_time=20e-6) # Parameters FOV = 250e-3 # Field of view #N_sample = int(N / 2) # Number of points sampled on a radial spoke / number of segments 1 pulse is broken into dx = FOV / N # Spatial resolution (isotropic) # Number of spokes, number of altitude angles, number of azimuthal angles if enc_type == '3D': Ns, Ntheta, Nphi = get_radk_params_3D(dx, FOV) N_line = Ntheta * Nphi # Radial encoding details thetas = np.linspace(0, np.pi, Ntheta, endpoint=False) elif enc_type == "2D": Nphi = get_radk_params_2D(N) thetas = np.array([np.pi / 2]) # Zero z-gradient. N_line = Nphi phis = np.linspace(0, 2 * np.pi, Nphi * spoke_os, endpoint=False) N_line = N_line * spoke_os # Pulse parameters FA = flip * pi / 180 beta = 5 # so that F1 is ~0.01 at tau = +- 1 Tp = R / BW # Total pulse duration N_seg = int(L_over * R) dw = Tp / N_seg # sampling dt dc = 0.5 # pulse duty cycle T_seg = dw * dc # Duration of 1 segment gap = dw - T_seg # time interval where RF is off in each segment adc_factor = 0.5 # Where during the gap lies the sample #g_amp = dk / dw # Net gradient amplitude g_amp = BW / FOV RF_amp_max = FA / (2 * pi * np.power(beta, -1 / 2) * np.sqrt(R) / BW ) # From gapped pulse paper; units: [Hz] # Gradient & ADC ramp_time = 100e-6 delay_TR = TR - N_seg * dw g_const = make_extended_trapezoid(channel='x', times=np.array([0, dw]), amplitudes=np.array([g_amp, g_amp]), system=system) g_delay = make_extended_trapezoid(channel='x', times=np.array([0, delay_TR]), amplitudes=np.array([g_amp, g_amp]), system=system) #dwell = ((1 - adc_factor) * gap - 200e-6) / 2 # This worked for 3D N=16 but not 2D N=128 #dwell = ((1 - adc_factor) * gap - system.adc_dead_time) / 2 #dwell = ((1 - adc_factor) * gap) / 2 dwell = 10e-6 adc = make_adc(num_samples=n_adc, delay=T_seg + adc_factor * gap, dwell=dwell) # Find RF modulations AM, FM = make_HS_pulse(beta=beta, b1=RF_amp_max, bw=BW) list_RFs = extract_chopped_pulses(AM, FM, Tp, N_seg, T_seg, fm_type=fm_type) # Save information rf_complex = np.zeros((1, len(list_RFs)), dtype=complex) for u in range(len(list_RFs)): rf_complex[0, u] = list_RFs[u].signal[0] * np.exp( 1j * list_RFs[u].phase_offset) amp_gx, amp_gy, amp_gz = 0, 0, 0 # Construct the sequence seq = Sequence(system) q = 0 y = 0 ktraj = np.zeros((N_seg, N_line, 3)) nline = 0 for theta in thetas: # For each altitude angle for phi in phis: # For each azimuth angle print(f'Adding {q} of {len(thetas)*len(phis)} directions') # Calculate gradients unit_grad = get_3d_unit_grad(theta, phi) g_const_x, g_const_y, g_const_z = make_oblique_gradients( g_const, unit_grad) # Ramps from last encode to this oen g_ramp_x = make_extended_trapezoid_check_zero( channel='x', times=np.array([0, ramp_time]), amplitudes=[amp_gx, g_const_x.first], system=system) g_ramp_y = make_extended_trapezoid_check_zero( channel='y', times=np.array([0, ramp_time]), amplitudes=[amp_gy, g_const_y.first], system=system) g_ramp_z = make_extended_trapezoid_check_zero( channel='z', times=np.array([0, ramp_time]), amplitudes=[amp_gz, g_const_z.first], system=system) amp_gx, amp_gy, amp_gz = g_const_x.first, g_const_y.first, g_const_z.first seq.add_block(g_ramp_x, g_ramp_y, g_ramp_z) for n in range(N_seg): # For each RF segment # Make ramp-up from the last gradient & this constant gradient # Add RF, ADC with delay, and split gradient together seq.add_block(list_RFs[n], g_const_x, g_const_y, g_const_z, adc) y = y + 1 dk_3d = adc.delay * np.array([ g_const_x.waveform[0], g_const_y.waveform[0], g_const_z.waveform[0] ]) ktraj[:, nline, 0] = dk_3d[0] * np.arange(1, N_seg + 1) ktraj[:, nline, 1] = dk_3d[1] * np.arange(1, N_seg + 1) ktraj[:, nline, 2] = dk_3d[2] * np.arange(1, N_seg + 1) nline += 1 g_delay_x, g_delay_y, g_delay_z = make_oblique_gradients( g_delay, unit_grad) seq.add_block(g_delay_x, g_delay_y, g_delay_z) q += 1 today = date.today() todayf = today.strftime("%m%d%y") seq_info = { 'thetas': thetas, 'phis': phis, 'rf_complex': rf_complex, 'ktraj': ktraj } savemat( f'swiftV2_info_bw{BW}_nadc{n_adc}_FOV{FOV*1e3}_FA{flip}_N{N}_{enc_type}_L-over{L_over}_{todayf}_sos{spoke_os}.mat', seq_info) if output: seq.write( f'swiftV2_bw{BW}_nadc{n_adc}_FOV{FOV*1e3}_FA{flip}_N{N}_{enc_type}_L-over{L_over}_{todayf}_sos{spoke_os}.seq' ) print(f'Gradient amplitude is {g_amp} Hz/m') return seq, seq_info