def plot(self, type: str = 'Gradient', time_range=(0, np.inf), time_disp: str = 's', save: bool = False): """ Plot `Sequence`. Parameters ---------- type : str Gradients display type, must be one of either 'Gradient' or 'Kspace'. time_range : List Time range (x-axis limits) for plotting the sequence. Default is 0 to infinity (entire sequence). time_disp : str Time display type, must be one of `s`, `ms` or `us`. save_as : bool Boolean flag indicating if plots should be saved. The two figures will be saved as JPG with numerical suffixes to the filename 'seq_plot'. """ valid_plot_types = ['Gradient', 'Kspace'] valid_time_units = ['s', 'ms', 'us'] if type not in valid_plot_types: raise Exception() if time_disp not in valid_time_units: raise Exception() fig1, fig2 = plt.figure(1), plt.figure(2) sp11 = fig1.add_subplot(311) sp12, sp13 = fig1.add_subplot(312, sharex=sp11), fig1.add_subplot( 313, sharex=sp11) fig2_sp_list = [ fig2.add_subplot(311, sharex=sp11), fig2.add_subplot(312, sharex=sp11), fig2.add_subplot(313, sharex=sp11) ] t_factor_list = [1, 1e3, 1e6] t_factor = t_factor_list[valid_time_units.index(time_disp)] t0 = 0 for iB in range(1, len(self.block_events) + 1): block = self.get_block(iB) is_valid = time_range[0] <= t0 <= time_range[1] if is_valid: if hasattr(block, 'adc'): adc = block.adc t = adc.delay + [(x * adc.dwell) for x in range(0, int(adc.num_samples))] sp11.plot((t0 + t), np.zeros(len(t)), 'rx') if hasattr(block, 'rf'): rf = block.rf tc, ic = calc_rf_center(rf) t = rf.t + rf.delay tc = tc + rf.delay sp12.plot(t_factor * (t0 + t), abs(rf.signal)) sp13.plot( t_factor * (t0 + t), np.angle( rf.signal * np.exp(1j * rf.phase_offset) * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)), t_factor * (t0 + tc), np.angle(rf.signal[ic] * np.exp(1j * rf.phase_offset) * np.exp(1j * 2 * math.pi * rf.t[ic] * rf.freq_offset)), 'xb') grad_channels = ['gx', 'gy', 'gz'] for x in range(0, len(grad_channels)): if hasattr(block, grad_channels[x]): grad = getattr(block, grad_channels[x]) if grad.type == 'grad': # In place unpacking of grad.t with the starred expression t = grad.delay + [ 0, *(grad.t + (grad.t[1] - grad.t[0]) / 2), grad.t[-1] + grad.t[1] - grad.t[0] ] waveform = np.array([grad.first, grad.last]) waveform = 1e-3 * np.insert( waveform, 1, grad.waveform) else: t = np.cumsum([ 0, grad.delay, grad.rise_time, grad.flat_time, grad.fall_time ]) waveform = 1e-3 * grad.amplitude * np.array( [0, 0, 1, 1, 0]) fig2_sp_list[x].plot(t_factor * (t0 + t), waveform) t0 += calc_duration(block) grad_plot_labels = ['x', 'y', 'z'] sp11.set_ylabel('ADC') sp12.set_ylabel('RF mag (Hz)') sp13.set_ylabel('RF phase (rad)') [ fig2_sp_list[x].set_ylabel(f'G{grad_plot_labels[x]} (kHz/m)') for x in range(3) ] # Setting display limits disp_range = t_factor * np.array( [time_range[0], min(t0, time_range[1])]) sp11.set_xlim(disp_range) sp12.set_xlim(disp_range) sp13.set_xlim(disp_range) [x.set_xlim(disp_range) for x in fig2_sp_list] fig1.tight_layout() fig2.tight_layout() if save: fig1.savefig('seq_plot1.jpg') fig2.savefig('seq_plot2.jpg') plt.show()
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 warnings.warn(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) delay_end = make_delay(5) for k_ex in range(n_ex): for s in range(n_slices): rfex.freq_offset = GS_ex.amplitude * slice_thickness * (s - (n_slices - 1) / 2) rfref.freq_offset = GS_ref.amplitude * slice_thickness * (s - (n_slices - 1) / 2) rfex.phase_offset = rfex_phase - 2 * math.pi * rfex.freq_offset * calc_rf_center(rfex)[0] rfref.phase_offset = rfref_phase - 2 * math.pi * rfref.freq_offset * calc_rf_center(rfref)[0] seq.add_block(GS1) seq.add_block(GS2, rfex) seq.add_block(GS3, GR3) for k_ech in range(n_echo): if k_ex >= 0: phase_area = phase_areas[k_ech] else: phase_area = 0 GP_pre = make_trapezoid(channel='y', system=system, area=phase_area, duration=t_sp, rise_time=dG) GP_rew = make_trapezoid(channel='y', system=system, area=-phase_area, duration=t_sp, rise_time=dG)
if TR_fill < 0: TR_fill = 1e-3 warnings.warn( 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) for k_ex in range(n_ex + 1): for s in range(n_slices): rf_ex.freq_offset = gs_ex.amplitude * slice_thickness * ( s - (n_slices - 1) / 2) rf_ref.freq_offset = gs_ref.amplitude * slice_thickness * ( 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] seq.add_block(gs1) seq.add_block(gs2, rf_ex) seq.add_block(gs3, gr3) for k_echo in range(n_echo): if k_ex > 0: phase_area = phase_areas[k_echo, k_ex - 1] else: phase_area = 0.0 # 0.0 and not 0 because -phase_area should successfully result in negative zero gp_pre = make_trapezoid(channel='y', system=system,
def calculate_kspace(self, trajectory_delay: int = 0): """ Calculates the k-space trajectory of the entire pulse sequence. Parameters ---------- trajectory_delay : int Compensation factor in millis to align ADC and gradients in the reconstruction. Returns ------- k_traj_adc : numpy.ndarray K-space trajectory sampled at `t_adc` timepoints. k_traj : numpy.ndarray K-space trajectory of the entire pulse sequence. t_excitation : numpy.ndarray Excitation timepoints. t_refocusing : numpy.ndarray Refocusing timepoints. t_adc : numpy.ndarray Sampling timepoints. """ c_excitation = 0 c_refocusing = 0 c_adc_samples = 0 for i in range(len(self.block_events)): block = self.get_block(i + 1) if hasattr(block, 'rf'): if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': c_excitation += 1 else: c_refocusing += 1 if hasattr(block, 'adc'): c_adc_samples += int(block.adc.num_samples) t_excitation = np.zeros(c_excitation) t_refocusing = np.zeros(c_refocusing) k_time = np.zeros(c_adc_samples) current_dur = 0 c_excitation = 0 c_refocusing = 0 k_counter = 0 traj_recon_delay = trajectory_delay for i in range(len(self.block_events)): block = self.get_block(i + 1) if hasattr(block, 'rf'): rf = block.rf rf_center, _ = calc_rf_center(rf) t = rf.delay + rf_center if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': t_excitation[c_excitation] = current_dur + t c_excitation += 1 else: t_refocusing[c_refocusing] = current_dur + t c_refocusing += 1 if hasattr(block, 'adc'): k_time[k_counter:k_counter + block.adc.num_samples] = np.arange( block.adc.num_samples ) * block.adc.dwell + block.adc.delay + current_dur + traj_recon_delay k_counter += block.adc.num_samples current_dur += calc_duration(block) gw = self.gradient_waveforms() i_excitation = np.round(t_excitation / self.grad_raster_time) i_refocusing = np.round(t_refocusing / self.grad_raster_time) k_traj = np.zeros(gw.shape) k = [0, 0, 0] for i in range(gw.shape[1]): k += gw[:, i] * self.grad_raster_time k_traj[:, i] = k if len(np.where(i_excitation == i + 1)[0]) >= 1: k = 0 k_traj[:, i] = np.nan if len(np.where(i_refocusing == i + 1)[0]) >= 1: k = -k k_traj_adc = [] for i in range(k_traj.shape[0]): k_traj_adc.append( np.interp( k_time, np.arange(1, k_traj.shape[1] + 1) * self.grad_raster_time, k_traj[i])) k_traj_adc = np.asarray(k_traj_adc) t_adc = k_time return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc
system=system, area=-(Ny / 2 - 0.5 - (Ny - Nyeff)) * delta_k, duration=pre_time) # Phase blip in shortest possible time gy = make_trapezoid(channel='y', system=system, area=delta_k) dur = math.ceil(calc_duration(gy) / seq.grad_raster_time) / i_raster_time # Integer times needed for TE optimization # The time(gy) refers to the number of blips, thus we substract 0.5 since the number of lines is always even. # The time(gx) refers to the time needed to read each line of the k-space. Thus, if Ny is even, it would take half of the lines plus another half. n_duration_center = math.ceil( (calc_duration(gx) * (Ny / 2 + 0.5 - (Ny - Nyeff)) + dur * (Ny / 2 - 0.5 - (Ny - Nyeff)) + calc_duration(gx_pre, gy_pre)) / seq.grad_raster_time) rf_center_with_delay = rf.delay + calc_rf_center(rf)[0] n_rf90r = math.ceil((calc_duration(gz) - rf_center_with_delay + pre_time) / seq.grad_raster_time) n_rf180r = math.ceil((calc_duration(rf180) + 2 * calc_duration(gz_spoil)) / 2 / seq.grad_raster_time) n_rf180l = math.floor((calc_duration(rf180) + 2 * calc_duration(gz_spoil)) / 2 / seq.grad_raster_time) # Group variables seq_sys_Dict = {"seq": seq, "system": system, "i_raster_time": i_raster_time} grads_times_Dict = { "n_rf90r": n_rf90r, "n_rf180r": n_rf180r, "n_rf180l": n_rf180l,
def calculate_kspace(self, trajectory_delay=0): c_excitation = 0 c_refocusing = 0 c_adc_samples = 0 for i in range(len(self.block_events)): block = self.get_block(i + 1) if hasattr(block, 'rf'): if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': c_excitation += 1 else: c_refocusing += 1 if hasattr(block, 'adc'): c_adc_samples += int(block.adc.num_samples) t_excitation = np.zeros(c_excitation) t_refocusing = np.zeros(c_refocusing) k_time = np.zeros(c_adc_samples) current_dur = 0 c_excitation = 0 c_refocusing = 0 k_counter = 0 traj_recon_delay = trajectory_delay for i in range(len(self.block_events)): block = self.get_block(i + 1) if hasattr(block, 'rf'): rf = block.rf rf_center, _ = calc_rf_center(rf) t = rf.delay + rf_center if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': t_excitation[c_excitation] = current_dur + t c_excitation += 1 else: t_refocusing[c_refocusing] = current_dur + t c_refocusing += 1 if hasattr(block, 'adc'): k_time[k_counter:k_counter + block.adc.num_samples] = np.arange( block.adc.num_samples ) * block.adc.dwell + block.adc.delay + current_dur + traj_recon_delay k_counter += block.adc.num_samples current_dur += calc_duration(block) gw = self.gradient_waveforms() i_excitation = np.round(t_excitation / self.grad_raster_time) i_refocusing = np.round(t_refocusing / self.grad_raster_time) k_traj = np.zeros(gw.shape) k = [0, 0, 0] for i in range(gw.shape[1]): k += gw[:, i] * self.grad_raster_time k_traj[:, i] = k if len(np.where(i_excitation == i + 1)[0]) >= 1: k = 0 k_traj[:, i] = np.nan if len(np.where(i_refocusing == i + 1)[0]) >= 1: k = -k k_traj_adc = [] for i in range(k_traj.shape[0]): k_traj_adc.append( np.interp( k_time, np.arange(1, k_traj.shape[1] + 1) * self.grad_raster_time, k_traj[i])) k_traj_adc = np.asarray(k_traj_adc) t_adc = k_time return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc
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 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 calculate_kspace(self, trajectory_delay: int = 0) -> Tuple[np.array, np.array, np.array, np.array, np.array]: """ Calculates the k-space trajectory of the entire pulse sequence. Parameters ---------- trajectory_delay : int, default=0 Compensation factor in millis to align ADC and gradients in the reconstruction. Returns ------- k_traj_adc : numpy.array K-space trajectory sampled at `t_adc` timepoints. k_traj : numpy.array K-space trajectory of the entire pulse sequence. t_excitation : numpy.array Excitation timepoints. t_refocusing : numpy.array Refocusing timepoints. t_adc : numpy.array Sampling timepoints. """ # Initialise the counters and accumulator objects count_excitation = 0 count_refocusing = 0 count_adc_samples = 0 # Loop through the blocks to prepare preallocations for block_counter in range(len(self.dict_block_events)): block = self.get_block(block_counter + 1) if hasattr(block, 'rf'): if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': count_excitation += 1 else: count_refocusing += 1 if hasattr(block, 'adc'): count_adc_samples += int(block.adc.num_samples) t_excitation = np.zeros(count_excitation) t_refocusing = np.zeros(count_refocusing) k_time = np.zeros(count_adc_samples) current_duration = 0 count_excitation = 0 count_refocusing = 0 kc_outer = 0 traj_recon_delay = trajectory_delay # Go through the blocks and collect RF and ADC timing data for block_counter in range(len(self.dict_block_events)): block = self.get_block(block_counter + 1) if hasattr(block, 'rf'): rf = block.rf rf_center, _ = calc_rf_center(rf) t = rf.delay + rf_center if not hasattr(block.rf, 'use') or block.rf.use != 'refocusing': t_excitation[count_excitation] = current_duration + t count_excitation += 1 else: t_refocusing[count_refocusing] = current_duration + t count_refocusing += 1 if hasattr(block, 'adc'): _k_time = np.arange(block.adc.num_samples) + 0.5 _k_time = _k_time * block.adc.dwell + block.adc.delay + current_duration + traj_recon_delay k_time[kc_outer:kc_outer + block.adc.num_samples] = _k_time kc_outer += block.adc.num_samples current_duration += self.arr_block_durations[block_counter] # Now calculate the actual k-space trajectory based on the gradient waveforms gw = self.gradient_waveforms() i_excitation = np.round(t_excitation / self.grad_raster_time) i_refocusing = np.round(t_refocusing / self.grad_raster_time) i_periods = np.sort([1, *(i_excitation + 1), *(i_refocusing + 1), gw.shape[1] + 1]).astype(np.int) # i_periods -= 1 # Python is 0-indexed ii_next_excitation = min(len(i_excitation), 1) ii_next_refocusing = min(len(i_refocusing), 1) k_traj = np.zeros_like(gw) k = np.zeros((3, 1)) for i in range(len(i_periods) - 1): i_period_end = i_periods[i + 1] - 1 k_period = np.concatenate((k, gw[:, i_periods[i] - 1:i_period_end] * self.grad_raster_time), axis=1) k_period = np.cumsum(k_period, axis=1) k_traj[:, i_periods[i] - 1:i_period_end] = k_period[:, 1:] k = k_period[:, -1] if ii_next_excitation > 0 and i_excitation[ii_next_excitation - 1] == i_period_end: k[:] = 0 k_traj[:, i_period_end - 1] = np.nan ii_next_excitation = min(len(i_excitation), ii_next_excitation + 1) if ii_next_refocusing > 0 and i_refocusing[ii_next_refocusing - 1] == i_period_end: k = -k ii_next_refocusing = min(len(i_refocusing), ii_next_refocusing + 1) k = k.reshape((-1, 1)) # To be compatible with np.concatenate k_traj_adc = [] for _k_traj_row in k_traj: result = np.interp(xp=np.array(range(1, k_traj.shape[1] + 1)) * self.grad_raster_time, fp=_k_traj_row, x=k_time) k_traj_adc.append(result) k_traj_adc = np.stack(k_traj_adc) t_adc = k_time return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc
def plot(self, label: str = str(), save: bool = False, time_range=(0, np.inf), time_disp: str = 's', plot_type: str = 'Gradient') -> None: """ Plot `Sequence`. Parameters ---------- label : str, defualt=str() save : bool, default=False Boolean flag indicating if plots should be saved. The two figures will be saved as JPG with numerical suffixes to the filename 'seq_plot'. time_range : iterable, default=(0, np.inf) Time range (x-axis limits) for plotting the sequence. Default is 0 to infinity (entire sequence). time_disp : str, default='s' Time display type, must be one of `s`, `ms` or `us`. plot_type : str, default='Gradient' Gradients display type, must be one of either 'Gradient' or 'Kspace'. """ mpl.rcParams['lines.linewidth'] = 0.75 # Set default Matplotlib linewidth valid_plot_types = ['Gradient', 'Kspace'] valid_time_units = ['s', 'ms', 'us'] valid_labels = get_supported_labels() if plot_type not in valid_plot_types: raise ValueError('Unsupported plot type') if not all([isinstance(x, (int, float)) for x in time_range]) or len(time_range) != 2: raise ValueError('Invalid time range') if time_disp not in valid_time_units: raise ValueError('Unsupported time unit') fig1, fig2 = plt.figure(1), plt.figure(2) sp11 = fig1.add_subplot(311) sp12, sp13 = fig1.add_subplot(312, sharex=sp11), fig1.add_subplot(313, sharex=sp11) fig2_subplots = [fig2.add_subplot(311, sharex=sp11), fig2.add_subplot(312, sharex=sp11), fig2.add_subplot(313, sharex=sp11)] t_factor_list = [1, 1e3, 1e6] t_factor = t_factor_list[valid_time_units.index(time_disp)] t0 = 0 label_defined = False label_idx_to_plot = [] label_legend_to_plot = [] label_store = dict() for i in range(len(valid_labels)): label_store[valid_labels[i]] = 0 if label.upper() == valid_labels[i]: label_idx_to_plot.append(i) label_legend_to_plot.append(valid_labels[i]) if len(label_idx_to_plot) != 0: p = parula.main(len(label_idx_to_plot) + 1) label_colors_to_plot = p(np.arange(len(label_idx_to_plot))) for block_counter in range(len(self.dict_block_events)): block = self.get_block(block_counter + 1) is_valid = time_range[0] <= t0 <= time_range[1] if is_valid: if hasattr(block, 'label'): for i in range(len(block.label)): if block.label[i].type == 'labelinc': label_store[block.label[i].label] += block.label[i].value else: label_store[block.label[i].label] = block.label[i].value label_defined = True if hasattr(block, 'adc'): adc = block.adc # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this # is the present convention - the samples are shifted by 0.5 dwell t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell sp11.plot(t_factor * (t0 + t), np.zeros(len(t)), 'rx') sp13.plot(t_factor * (t0 + t), np.angle(np.exp(1j * adc.phase_offset) * np.exp(1j * 2 * np.pi * t * adc.freq_offset)), 'b.') if label_defined and len(label_idx_to_plot) != 0: cycler = mpl.cycler(color=label_colors_to_plot) sp11.set_prop_cycle(cycler) label_store_arr = list(label_store.values()) lbl_vals = np.take(label_store_arr, label_idx_to_plot) t = t0 + adc.delay + (adc.num_samples - 1) / 2 * adc.dwell p = sp11.plot(t_factor * t, lbl_vals, '.') if len(label_legend_to_plot) != 0: sp11.legend(p, label_legend_to_plot, loc='upper left') label_legend_to_plot = [] if hasattr(block, 'rf'): rf = block.rf tc, ic = calc_rf_center(rf) t = rf.t + rf.delay tc = tc + rf.delay sp12.plot(t_factor * (t0 + t), np.abs(rf.signal)) sp13.plot(t_factor * (t0 + t), np.angle(rf.signal * np.exp(1j * rf.phase_offset) * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)), t_factor * (t0 + tc), np.angle(rf.signal[ic] * np.exp(1j * rf.phase_offset) * np.exp(1j * 2 * math.pi * rf.t[ic] * rf.freq_offset)), 'xb') grad_channels = ['gx', 'gy', 'gz'] for x in range(len(grad_channels)): if hasattr(block, grad_channels[x]): grad = getattr(block, grad_channels[x]) if grad.type == 'grad': # In place unpacking of grad.t with the starred expression t = grad.delay + [0, *(grad.t + (grad.t[1] - grad.t[0]) / 2), grad.t[-1] + grad.t[1] - grad.t[0]] waveform = 1e-3 * np.array((grad.first, *grad.waveform, grad.last)) else: t = np.cumsum([0, grad.delay, grad.rise_time, grad.flat_time, grad.fall_time]) waveform = 1e-3 * grad.amplitude * np.array([0, 0, 1, 1, 0]) fig2_subplots[x].plot(t_factor * (t0 + t), waveform) t0 += self.arr_block_durations[block_counter] grad_plot_labels = ['x', 'y', 'z'] sp11.set_ylabel('ADC') sp12.set_ylabel('RF mag (Hz)') sp13.set_ylabel('RF/ADC phase (rad)') sp13.set_xlabel('t(s)') for x in range(3): _label = grad_plot_labels[x] fig2_subplots[x].set_ylabel(f'G{_label} (kHz/m)') fig2_subplots[-1].set_xlabel('t(s)') # Setting display limits disp_range = t_factor * np.array([time_range[0], min(t0, time_range[1])]) [x.set_xlim(disp_range) for x in [sp11, sp12, sp13, *fig2_subplots]] fig1.tight_layout() fig2.tight_layout() if save: fig1.savefig('seq_plot1.jpg') fig2.savefig('seq_plot2.jpg') plt.show()
# Pre-phasing gradients pre_time = 1e-3 gx_pre = make_trapezoid(channel='x', system=system, area=-gx.area / 2, duration=pre_time) gz_reph = make_trapezoid(channel='z', system=system, area=-gz.area / 2, duration=pre_time) gy_pre = make_trapezoid(channel='y', system=system, area=-(Ny / 2 - 0.5 - (Ny - Nyeff)) * delta_k, duration=pre_time) # Phase blip in shortest possible time gy = make_trapezoid(channel='y', system=system, area=delta_k) dur = math.ceil(calc_duration(gy) / seq.grad_raster_time) * seq.grad_raster_time # Calculate some times constant throughout the process # The time(gy) refers to the number of blips, thus we substract 0.5 since the number of lines is always even. # The time(gx) refers to the time needed to read each line of the k-space. Thus, if Ny is even, it would take half of the lines plus another half. duration_center = (calc_duration(gx)*(Ny/2 + 0.5 - (Ny - Nyeff)) + dur * (Ny/2 - 0.5 - (Ny - Nyeff)) + calc_duration(gx_pre, gy_pre)) rf_center_with_delay = rf.delay + calc_rf_center(rf)[0] rf180_center_with_delay = rf180.delay + calc_rf_center(rf180)[0] # Group variables seq_sys_Dict = {"seq" : seq, "system" : system} grads_times_Dict = {"rf180" : rf180, "rf180_center_with_delay" : rf180_center_with_delay, "rf_center_with_delay" : rf_center_with_delay, "gz_spoil_1": gz_spoil_1, "gz_spoil_2" : gz_spoil_2, "gz" : gz, "duration_center" : duration_center, "pre_time": pre_time}