def align(*args) -> list: """ Aligns `SimpleNamespace` blocks as per specified alignment options by setting delays of the pulse sequence events within the block. All previously configured delays within objects are taken into account during calculating of the block duration but then reset according to the selected alignment. Possible values for align_spec are 'left', 'center', 'right'. Parameters ---------- args : list List of alignment options and `SimpleNamespace` blocks. Template: [alignment_spec, 'SimpleNamespace` block, [alignment_spec, `SimpleNamespace` block, ...]]. Alignment spec can be one of `left`, `center` or `right`. Returns ------- objects : list List of aligned `SimpleNamespace` blocks. """ alignment_options = ['left', 'center', 'right'] if not isinstance(args[0], str): raise ValueError('First parameter must be of type str.') curr_align = alignment_options.index( args[0]) if args[0] in alignment_options else None i_objects = [] alignments = [] for i in range(1, len(args)): if curr_align is None: raise ValueError('Invalid alignment spec.') if isinstance(args[i], str): curr_align = alignment_options.index( args[i]) if args[i] in alignment_options else None continue i_objects.append(i) alignments.append(curr_align) args = np.array(args) objects = args[i_objects] dur = calc_duration(*objects) for i in range(len(objects)): if alignments[i] == 0: objects[i].delay = 0 elif alignments[i] == 1: objects[i].delay = (dur - calc_duration(objects[i])) / 2 elif alignments[i] == 2: objects[i].delay = dur - calc_duration( objects[i]) + objects[i].delay return objects
def gradient_waveforms(self): duration, num_blocks, _ = self.duration() wave_length = math.ceil(duration / self.grad_raster_time) grad_channels = 3 grad_waveforms = np.zeros((grad_channels, wave_length)) grad_channels = ['gx', 'gy', 'gz'] t0 = 0 t0_n = 0 for i in range(num_blocks): block = self.get_block(i + 1) for j in range(len(grad_channels)): if hasattr(block, grad_channels[j]): grad = getattr(block, grad_channels[j]) if grad.type == 'grad': nt_start = round( (grad.delay + grad.t[0]) / self.grad_raster_time) waveform = grad.waveform else: nt_start = round(grad.delay / self.grad_raster_time) if abs(grad.flat_time) > np.finfo(float).eps: t = np.cumsum([ 0, grad.rise_time, grad.flat_time, grad.fall_time ]) trap_form = np.multiply([0, 1, 1, 0], grad.amplitude) else: t = np.cumsum([0, grad.rise_time, grad.fall_time]) trap_form = np.multiply([0, 1, 0], grad.amplitude) tn = math.floor(t[-1] / self.grad_raster_time) t = np.append(t, t[-1] + self.grad_raster_time) trap_form = np.append(trap_form, 0) if abs(grad.amplitude) > np.finfo(float).eps: waveform = points_to_waveform( t, trap_form, self.grad_raster_time) else: waveform = np.zeros(tn + 1) if waveform.size != np.sum(np.isfinite(waveform)): raise Warning( 'Not all elements of the generated waveform are finite' ) grad_waveforms[ j, int(t0_n + nt_start):int(t0_n + nt_start + max(waveform.shape))] = waveform t0 += calc_duration(block) t0_n = round(t0 / self.grad_raster_time) return grad_waveforms
def duration(self): num_blocks = len(self.block_events) event_count = np.zeros(len(self.block_events[1])) duration = 0 for i in range(num_blocks): block = self.get_block(i + 1) event_count += self.block_events[i + 1] > 0 duration += calc_duration(block) return duration, num_blocks, event_count
def _SAR_from_seq( seq: Sequence, Qtmf: np.ndarray, Qhmf: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Compute global whole body and head only SAR values for the given `seq` object. Parameters ---------- seq : Sequence Sequence object to calculate for which SAR values will be calculated. Qtmf : numpy.ndarray Q-matrix of global SAR values for body-mass. Qhmf : numpy.ndarray Q-matrix of global SAR values for head-mass. Returns ------- SAR_wbg : numpy.ndarray SAR values for body-mass. SAR_hg : numpy.ndarray SAR values for head-mass. t : numpy.ndarray Corresponding time points. """ # 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) block_events = seq.dict_block_events num_events = len(block_events) t = np.zeros(num_events) SAR_wbg = np.zeros(t.shape) SAR_hg = np.zeros(t.shape) t_prev = 0 for block_counter in block_events: block = seq.get_block(block_counter) block_dur = calc_duration(block) t[block_counter - 1] = t_prev + block_dur t_prev = t[block_counter - 1] if hasattr(block, 'rf'): # has rf rf = block.rf signal = rf.signal # This rf could be parallel transmit as well SAR_wbg[block_counter] = _calc_SAR(Qtmf, signal) SAR_hg[block_counter] = _calc_SAR(Qhmf, signal) return SAR_wbg, SAR_hg, t
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 check_timing(system, *events): total_dur = calc_duration(*events) is_ok = __div_check(total_dur, system.grad_raster_time) if is_ok: text_err = str() else: text_err = f'Total duration: {total_dur * 1e6} us' for i in range(len(events)): e = events[i] ok = True if hasattr(e, 'type') and e.type == 'adc' or e.type == 'rf': raster = system.rf_raster_time else: raster = system.grad_raster_time if hasattr(e, 'delay'): if not __div_check(e.delay, raster): ok = False if hasattr(e, 'type') and e.type == 'trap': if not __div_check(e.rise_time, system.grad_raster_time) or \ not __div_check(e.flat_time, system.grad_raster_time) or \ not __div_check(e.fall_time, system.grad_raster_time): ok = False if not ok: is_ok = False if len(text_err) != 0: text_err += ' ' text_err += '[ ' if hasattr(e, 'type'): text_err += f'type: {e.type} ' if hasattr(e, 'delay'): text_err += f'delay: {e.delay * 1e6} us ' if hasattr(e, 'type') and e.type == 'trap': text_err += f'rise time: {e.rise_time * 1e6} flat time: {e.flat_time * 1e6} ' \ f'fall time: {e.fall_time * 1e6} us ' text_err += ']' return is_ok, text_err
def __SAR_from_seq(seq, Qtmf, Qhmf): """ Compute global whole body and head only SAR values. Parameters ---------- seq : Sequence Qtmf : numpy.ndarray Qhmf : numpy.ndarray Returns ------- SAR_wbg_vec : numpy.ndarray SAR_hg_vec : numpy.ndarray t_vec : numpy.ndarray Contains the Q-matrix, GSAR head and body for now. """ # 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) block_events = seq.block_events num_events = len(block_events) t_vec = np.zeros(num_events) SAR_wbg_vec = np.zeros(t_vec.shape) SAR_hg_vec = np.zeros(t_vec.shape) t_prev = 0 for iB in block_events: block = seq.get_block(iB) block_dur = calc_duration(block) t_vec[iB - 1] = t_prev + block_dur t_prev = t_vec[iB - 1] if hasattr(block, 'rf'): # has rf rf = block.rf t = rf.t signal = rf.signal # This rf could be parallel transmit as well SAR_wbg_vec[iB] = __calc_SAR(Qtmf, signal) SAR_hg_vec[iB] = __calc_SAR(Qhmf, signal) return SAR_wbg_vec, SAR_hg_vec, t_vec
def duration(self): """ Get duration of this sequence. Returns ------- duration : float Duration of this sequence in millis. num_blocks : int Number of blocks in this sequence. event_count : int Number of events in this sequence. """ num_blocks = len(self.block_events) event_count = np.zeros(len(self.block_events[1])) duration = 0 for i in range(num_blocks): block = self.get_block(i + 1) event_count += self.block_events[i + 1] > 0 duration += calc_duration(block) return duration, num_blocks, event_count
gdir, nb0s = difunc.get_dirs(ndirs) #to avoid exceeding gradient limits gdfact = 1.0 tr_per_slice = tr / n_slices 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) b0 = 2.89 sat_ppm = -3.45 sat_freq = sat_ppm * 1e-6 * b0 * system.gamma rf_fs, _, _ = make_gauss_pulse(flip_angle=110 * math.pi / 180, system=system, duration=8e-3, bandwidth=abs(sat_freq), freq_offset=sat_freq) gz_fs = make_trapezoid(channel='z', system=system, delay=calc_duration(rf_fs), area=1 / 1e-4) rf, gz, gz_reph = make_sinc_pulse(flip_angle=math.pi / 2, system=system, duration=3e-3, slice_thickness=slice_thickness, apodization=0.5, time_bw_product=4) rf180, gz180, _ = make_sinc_pulse(flip_angle=math.pi, system=system, duration=5e-3, slice_thickness=slice_thickness, apodization=0.5, time_bw_product=4) rf180.phase_offset = math.pi/2 gz_spoil = make_trapezoid(channel='z', system=system, area=2*gz.area, duration=3e-3) delta_k = 1 / fov k_width = Nx * delta_k readout_time = 1.05e-3 blip_dur = math.ceil(2 * math.sqrt(delta_k / system.max_slew) / seq.grad_raster_time / 2) * seq.grad_raster_time * 2
def opt_TE_bv_TRSE(bvalue_Dict, grads_times_Dict, seq_sys_Dict): """ Obtain optimal TE and diffusion-weighting gradient waveforms for twice-refocued spin echo (TRSE) DW sequence. Parameters ---------- bvalue_Dict : dictionary b-value related parameters needed in the TE optimization. grads_times_Dict : dictionary gradient times related parameters needed in the TE optimization. seq_sys_Dict : dictionary sequence and system related parameters needed in the TE optimization. Returns ------- TE : float echo time (s). bval : float b-value in s/mm2 waveform : float diffusion-weighting gradient waveform. gscl_max : float gradient scale factor of the diffusion-weighting gradient waveform. d1 : float duration of the first diffusion-weighting gradient lobe between the RF90 and the first RF180 (s). d2 : float duration of the second diffusion-weighting gradient lobe after the first RF180 (s). d3 : float duration of the third diffusion-weighting gradient lobe before the second RF180 (s). d4 : float duration of the last diffusion-weighting gradient lobe after the second RF180 (s). """ # Description # For TRSE waveforms. # From an initial TE, check we satisfy all constraints -> otherwise increase TE. # Once all constraints are okay -> check b-value, if it is lower than the target one -> increase TE # Finally, with TRSE low b-values can not be acquired, thus proper scaling is needed. # Looks time-inefficient but it is fast enough to make it user-friendly. bvalue = bvalue_Dict["bvalue"] # Target b-value. nbvals = bvalue_Dict["nbvals"] # number of b-values. gscl = bvalue_Dict["gscl"] # gradient scaling. rf180 = grads_times_Dict["rf180"] # RF180 rf180_center_with_delay = grads_times_Dict[ "rf180_center_with_delay"] # time to the center of the RF180 rf_center_with_delay = grads_times_Dict[ "rf_center_with_delay"] # time to the center of the RF90 gz_spoil_1 = grads_times_Dict[ "gz_spoil_1"] # Spoil gradient to be placed around the first RF180 gz_spoil_2 = grads_times_Dict[ "gz_spoil_2"] # Spoil gradient to be placed around the second RF180 gz = grads_times_Dict["gz"] # RF90 gradient duration_center = grads_times_Dict[ "duration_center"] # Time needed to achieve the center of the k-space. pre_time = grads_times_Dict["pre_time"] # (s) seq = seq_sys_Dict["seq"] # Sequence system = seq_sys_Dict["system"] # System # Find minimum TE considering the readout times and RF + spoil gradient durations TE = 80e-3 # [s] It's a reasonable estimate for TRSE. delay_tela = -1 # Large TE/2 while delay_tela <= 0: TE = TE + 0.02e-3 # [ms] delay_tela = math.ceil((TE / 2 + (- calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_2) - \ duration_center) + (- calc_duration(gz) + rf_center_with_delay - \ pre_time - calc_duration(gz_spoil_1) - rf180_center_with_delay)) / seq.grad_raster_time) * seq.grad_raster_time # Sice there are 3 equations and 4 parameters (TGReese2002 - https://doi.org/10.1002/mrm.10308) # One parameter can be tuned, while the other 3 are then fixed. This is why we fix d4. d4 = 3e-3 # [ms] # Find minimum TE for the target d4 # Waveform Ramp time gdiff_rt = math.ceil(system.max_grad / system.max_slew / seq.grad_raster_time) * seq.grad_raster_time d1 = -1 while d1 <= 2 * gdiff_rt: # Include this condition to have trapezoids everywhere TE = TE + 2 * seq.grad_raster_time # [ms] # Large TE/2 delay_tela = math.ceil((TE / 2 + (- calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_2) - \ duration_center) + (- calc_duration(gz) + rf_center_with_delay - \ pre_time - calc_duration(gz_spoil_1) - rf180_center_with_delay)) / seq.grad_raster_time) * seq.grad_raster_time # Short TE/2 (Time between RF180s) delay_tes = math.ceil((TE / 2 - calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_1) - \ calc_duration(gz_spoil_2) - rf180_center_with_delay) / seq.grad_raster_time) * seq.grad_raster_time d1 = math.ceil( (delay_tela - d4) / seq.grad_raster_time) * seq.grad_raster_time # Find minimum TE for the target b-value bvalue_tmp = 0 # We initially substract 2 times the raster time because we don't know if it will achieve the desired b-value from the beginning. # Note that for low b-values, the TE is the same for all of them due to the large number of elements of this sequence. TE = TE - 2 * seq.grad_raster_time while bvalue_tmp < np.max(bvalue): # The following scalar multiplication is just to increase speed TE = TE + int(math.ceil( np.max(bvalue) / 500)) * 4 * seq.grad_raster_time # [ms] # Large TE/2 delay_tela = math.ceil((TE / 2 + (- calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_2) - \ duration_center) + (- calc_duration(gz) + rf_center_with_delay - \ pre_time - calc_duration(gz_spoil_1) - rf180_center_with_delay)) / seq.grad_raster_time) * seq.grad_raster_time # Short TE/2 (Time between RF180s) delay_tes = math.ceil((TE / 2 - calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_1) - \ calc_duration(gz_spoil_2) - rf180_center_with_delay) / seq.grad_raster_time) * seq.grad_raster_time d1 = math.ceil( (delay_tela - d4) / seq.grad_raster_time) * seq.grad_raster_time d3 = math.ceil(((d1 + delay_tes - d4) / 2) / seq.grad_raster_time) * seq.grad_raster_time d2 = math.ceil( (delay_tes - d3) / seq.grad_raster_time) * seq.grad_raster_time # Due to the complexity of the sequence and that I have not found its b-value, I implement it by its definition. # However, for simplicity I compute each gradient lobe as ideal rectangular pulses. # d1 start after the RF90 n1 = int( math.ceil((calc_duration(gz) - rf_center_with_delay + pre_time + seq.grad_raster_time) / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) nd1 = int( math.ceil(d1 / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) # d2 start after the first RF180 n2 = int( math.ceil(((n1 + nd1) * seq.grad_raster_time + 2 * calc_duration(gz_spoil_1) + calc_duration(rf180) + seq.grad_raster_time) / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) nd2 = int( math.ceil(d2 / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) # d3 starts after d2 n3 = int( math.ceil( ((n2 + nd2) * seq.grad_raster_time + seq.grad_raster_time) / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) nd3 = int( math.ceil(d3 / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) # d4 starts after the second RF180 n4 = int( math.ceil(((n2 + nd2 + nd3) * seq.grad_raster_time + 2 * calc_duration(gz_spoil_2) + calc_duration(rf180) + seq.grad_raster_time) / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) nd4 = int( math.ceil(d4 / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) # Due to the large TE of TRSE and the high resolution we might need to shrink the following array. # To speed up computation we calculate the b-value with short_f times lower of the time resolution. # Shortening factor: short_f = 5 n = int(np.ceil(TE / seq.grad_raster_time) / short_f) waveform = np.zeros(n) # Compose waveform waveform[math.ceil(n1 / short_f):math.floor((n1 + nd1 + 1) / short_f)] = system.max_grad waveform[math.ceil(n2 / short_f):math.floor((n2 + nd2 + 1) / short_f)] = -system.max_grad waveform[math.ceil(n3 / short_f):math.floor((n3 + nd3 + 1) / short_f)] = system.max_grad waveform[math.ceil(n4 / short_f):math.floor((n4 + nd4 + 1) / short_f)] = -system.max_grad # Include ramps nrt = int( math.floor( math.floor( system.max_grad / system.max_slew / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time / short_f)) ramp_values = np.linspace( 1, nrt, nrt) * seq.grad_raster_time * system.max_slew * short_f # d1 if n1 % short_f == 0: waveform[math.ceil(n1 / short_f):math.ceil(n1 / short_f) + nrt] = ramp_values else: waveform[math.ceil(n1 / short_f):math.ceil((n1 + 1) / short_f) + nrt] = ramp_values waveform[math.ceil((n1 + nd1 - 1) / short_f - nrt):math.ceil((n1 + nd1 - 1) / short_f)] = np.flip(ramp_values) # d2 if n2 % short_f == 0: waveform[math.ceil(n2 / short_f):math.ceil(n2 / short_f) + nrt] = -ramp_values else: waveform[math.ceil(n2 / short_f):math.ceil((n2 + 1) / short_f) + nrt] = -ramp_values waveform[math.ceil((n2 + nd2 - 1) / short_f - nrt):math.ceil((n2 + nd2 - 1) / short_f)] = -np.flip(ramp_values) # d3 if n3 % short_f == 0: waveform[math.ceil(n3 / short_f):math.ceil(n3 / short_f) + nrt] = ramp_values else: waveform[math.ceil(n3 / short_f):math.ceil((n3 + 1) / short_f) + nrt] = ramp_values waveform[math.ceil((n3 + nd3 - 1) / short_f - nrt):math.ceil((n3 + nd3 - 1) / short_f)] = np.flip(ramp_values) # d4 if n4 % short_f == 0: waveform[math.ceil(n4 / short_f):math.ceil(n4 / short_f) + nrt] = -ramp_values else: waveform[math.ceil(n4 / short_f):math.ceil((n4 + 1) / short_f) + nrt] = -ramp_values waveform[math.ceil((n4 + nd4 - 1) / short_f - nrt):math.ceil((n4 + nd4 - 1) / short_f)] = -np.flip(ramp_values) # Prepare Integral nRF180_1 = int( math.ceil((n2 * seq.grad_raster_time - calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_1)) / short_f / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) nRF180_2 = int( math.ceil((n4 * seq.grad_raster_time - calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_2)) / short_f / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) INV = np.ones(n) INV[nRF180_1:nRF180_2 + 1] = -1 bvalue_tmp = calc_exact_bval(waveform, INV, short_f, seq) print("b-value low time resolution:", round(bvalue_tmp, 2), "s/mm2") # To know the exact b-value - repeat the above with full time resolution short_f = 1 n = int(np.ceil(TE / seq.grad_raster_time) / short_f) waveform = np.zeros(n) # Compose waveform waveform[math.ceil(n1 / short_f):math.floor((n1 + nd1 + 1) / short_f)] = system.max_grad waveform[math.ceil(n2 / short_f):math.floor((n2 + nd2 + 1) / short_f)] = -system.max_grad waveform[math.ceil(n3 / short_f):math.floor((n3 + nd3 + 1) / short_f)] = system.max_grad waveform[math.ceil(n4 / short_f):math.floor((n4 + nd4 + 1) / short_f)] = -system.max_grad # Include ramps nrt = int( math.floor( math.floor( system.max_grad / system.max_slew / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time / short_f)) ramp_values = np.linspace( 0, nrt, nrt) * seq.grad_raster_time * system.max_slew * short_f # d1 if n1 % short_f == 0: waveform[math.ceil(n1 / short_f):math.ceil(n1 / short_f) + nrt] = ramp_values else: waveform[math.ceil(n1 / short_f):math.ceil((n1 + 1) / short_f) + nrt] = ramp_values if (n1 + nd1) % short_f == 0: waveform[math.ceil((n1 + nd1) / short_f - nrt) + 1:math.floor((n1 + nd1 + 1) / short_f)] = np.flip(ramp_values) else: waveform[math.ceil((n1 + nd1) / short_f - nrt):math.floor((n1 + nd1 + 1) / short_f)] = np.flip(ramp_values) # d2 if n2 % short_f == 0: waveform[math.ceil(n2 / short_f):math.ceil(n2 / short_f) + nrt] = -ramp_values else: waveform[math.ceil(n2 / short_f):math.ceil((n2 + 1) / short_f) + nrt] = -ramp_values if (n2 + nd2) % short_f == 0: waveform[math.ceil((n2 + nd2) / short_f - nrt) + 1:math.floor((n2 + nd2 + 1) / short_f)] = -np.flip(ramp_values) else: waveform[math.ceil((n2 + nd2) / short_f - nrt):math.floor((n2 + nd2 + 1) / short_f)] = -np.flip(ramp_values) # d3 if n3 % short_f == 0: waveform[math.ceil(n3 / short_f):math.ceil(n3 / short_f) + nrt] = ramp_values else: waveform[math.ceil(n3 / short_f):math.ceil((n3 + 1) / short_f) + nrt] = ramp_values if (n3 + nd3) % short_f == 0: waveform[math.ceil((n3 + nd3) / short_f - nrt) + 1:math.floor((n3 + nd3 + 1) / short_f)] = np.flip(ramp_values) else: waveform[math.ceil((n3 + nd3) / short_f - nrt):math.floor((n3 + nd3 + 1) / short_f)] = np.flip(ramp_values) # d4 if n4 % short_f == 0: waveform[math.ceil(n4 / short_f):math.ceil(n4 / short_f) + nrt] = -ramp_values else: waveform[math.ceil(n4 / short_f):math.ceil((n4 + 1) / short_f) + nrt] = -ramp_values if (n4 + nd4) % short_f == 0: waveform[math.ceil((n4 + nd4) / short_f - nrt) + 1:math.floor((n4 + nd4 + 1) / short_f)] = -np.flip(ramp_values) else: waveform[math.ceil((n4 + nd4) / short_f - nrt):math.floor((n4 + nd4 + 1) / short_f)] = -np.flip(ramp_values) # Prepare Integral nRF180_1 = int( math.ceil((n2 * seq.grad_raster_time - calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_1)) / short_f / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) nRF180_2 = int( math.ceil((n4 * seq.grad_raster_time - calc_duration(rf180) + rf180_center_with_delay - calc_duration(gz_spoil_2)) / short_f / seq.grad_raster_time) * seq.grad_raster_time / seq.grad_raster_time) INV = np.ones(n) INV[nRF180_1:nRF180_2 + 1] = -1 bval = calc_exact_bval(waveform, INV, short_f, seq) print("b-value high time resolution:", round(bval, 2), "s/mm2") # Scale gradients amplitude accordingly. if bval > np.max(bvalue): gscl_max = np.sqrt(np.max(bvalue) / bval) else: gscl_max = 1 # Show final TE and b-values: print("Final times:") print("TE:", round(TE * 1e3, 2), "ms") for bv in range(1, nbvals + 1): bval_tmp = calc_exact_bval(waveform * gscl_max * gscl[bv], INV, short_f, seq) print(round(bval_tmp, 2), "s/mm2") return TE, bval, waveform, gscl_max, d1, d2, d3, d4
def opt_TE_bv_SE(bvalue_Dict, grads_times_Dict, seq_sys_Dict): """ Obtain optimal TE and diffusion-weighting gradient waveforms for twice-refocued spin echo (TRSE) DW sequence. Parameters ---------- bvalue_Dict : dictionary b-value related parameters needed in the TE optimization. grads_times_Dict : dictionary gradient times related parameters needed in the TE optimization. seq_sys_Dict : dictionary sequence and system related parameters needed in the TE optimization. Returns ------- n_TE : int echo time in integers units. bval : float b-value in s/mm2 gdiff : pypulseq gradient diffusion-weighting gradient waveform. n_delay_te1 : int delay between RF90 and RF180 in integers units. n_delay_te2 : int delay between RF180 and EPI readout in integers units. """ # Description # For S&T monopolar waveforms. # From an initial TE, check we satisfy all constraints -> otherwise increase TE. # Once all constraints are okay -> check b-value, if it is lower than the target one -> increase TE # Looks time-inefficient but it is fast enough to make it user-friendly. # We need to compute the exact time sequence. For the normal SE-MONO-EPI sequence micro second differences # are not important, however, if we wanna import external gradients the allocated time for them needs to # be the same, and thus exact timing is mandatory. With this in mind, we establish the following rounding rules: # Duration of RFs + spoiling, and EPI time to the center of the k-space is always math.ceil(). bvalue = bvalue_Dict["bvalue"] # Target b-value. nbvals = bvalue_Dict["nbvals"] # number of b-values. gscl = bvalue_Dict["gscl"] # gradient scaling. n_rf90r = grads_times_Dict[ "n_rf90r"] # Half right duration of RF90 (integer). n_rf180r = grads_times_Dict[ "n_rf180r"] # Half right duration of the RF180 (integer). n_rf180l = grads_times_Dict[ "n_rf180l"] # Half left duration of the RF180 (integer). gz_spoil = grads_times_Dict[ "gz_spoil"] # Spoil gradient to be placed around the RF180. gz180 = grads_times_Dict["gz180"] # RF180 simultaneous gradient. n_duration_center = grads_times_Dict[ "n_duration_center"] # Time needed to achieve the center of the k-space (integer). seq = seq_sys_Dict["seq"] # Sequence system = seq_sys_Dict["system"] # System i_raster_time = seq_sys_Dict[ "i_raster_time"] # Manually inputed inverse raster time. # Find minimum TE considering the readout times. n_TE = math.ceil(40e-3 / seq.grad_raster_time) n_delay_te2 = -1 while n_delay_te2 <= 0: n_TE = n_TE + 2 n_tINV = math.floor(n_TE / 2) n_delay_te2 = n_tINV - n_rf180r - n_duration_center # Find minimum TE for the target b-value bvalue_tmp = 0 while bvalue_tmp < np.max(bvalue): n_TE = n_TE + 2 n_tINV = math.floor(n_TE / 2) n_delay_te1 = n_tINV - n_rf90r - n_rf180l n_delay_te2 = n_tINV - n_rf180r - n_duration_center # Waveform Ramp time n_gdiff_rt = math.ceil(system.max_grad / system.max_slew / seq.grad_raster_time) # Select the shortest available time n_gdiff_delta = min(n_delay_te1, n_delay_te2) n_gdiff_Delta = n_delay_te1 + 2 * math.ceil( calc_duration(gz_spoil) / seq.grad_raster_time) + math.ceil( calc_duration(gz180) / seq.grad_raster_time) gdiff = make_trapezoid(channel='x', system=system, amplitude=system.max_grad, duration=n_gdiff_delta / i_raster_time) # delta only corresponds to the rectangle. n_gdiff_delta = n_gdiff_delta - 2 * n_gdiff_rt bv = calc_bval(system.max_grad, n_gdiff_delta / i_raster_time, n_gdiff_Delta / i_raster_time, n_gdiff_rt / i_raster_time) bval = bv * 1e-6 # Show final TE and b-values: print("TE:", round(n_TE / i_raster_time * 1e3, 2), "ms") for bv in range(1, nbvals + 1): print( round( calc_bval(system.max_grad * gscl[bv], n_gdiff_delta / i_raster_time, n_gdiff_Delta / i_raster_time, n_gdiff_rt / i_raster_time) * 1e-6, 2), "s/mm2") return n_TE, bval, gdiff, n_delay_te1, n_delay_te2
def split_gradient(grad: SimpleNamespace, system: Opts = Opts()) -> Tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace]: """ Split gradient waveform `grad` into two gradient waveforms at the center. Parameters ---------- grad : array_like Gradient waveform to be split into two gradient waveforms. system : Opts, optional, default=Opts() System limits. Returns ------- grad1, grad2 : numpy.ndarray Split gradient waveforms. Raises ------ ValueError If arbitrary gradients are passed. If non-gradient event is passed. """ grad_raster_time = system.grad_raster_time total_length = calc_duration(grad) if grad.type == 'trap': ch = grad.channel grad.delay = round(grad.delay / grad_raster_time) * grad_raster_time grad.rise_time = round(grad.rise_time / grad_raster_time) * grad_raster_time grad.flat_time = round(grad.flat_time / grad_raster_time) * grad_raster_time grad.fall_time = round(grad.fall_time / grad_raster_time) * grad_raster_time times = [0, grad.rise_time] amplitudes = [0, grad.amplitude] ramp_up = make_extended_trapezoid(channel=ch, system=system, times=times, amplitudes=amplitudes, skip_check=True) ramp_up.delay = grad.delay times = [0, grad.fall_time] amplitudes = [grad.amplitude, 0] ramp_down = make_extended_trapezoid(channel=ch, system=system, times=times, amplitudes=amplitudes, skip_check=True) ramp_down.delay = total_length - grad.fall_time ramp_down.t = ramp_down.t * grad_raster_time flat_top = SimpleNamespace() flat_top.type = 'grad' flat_top.channel = ch flat_top.delay = grad.delay + grad.rise_time flat_top.t = np.arange(step=grad_raster_time, stop=ramp_down.delay - grad_raster_time - grad.delay - grad.rise_time) flat_top.waveform = grad.amplitude * np.ones(len(flat_top.t)) flat_top.first = grad.amplitude flat_top.last = grad.amplitude return ramp_up, flat_top, ramp_down elif grad.type == 'grad': raise ValueError('Splitting of arbitrary gradients is not implemented yet.') else: raise ValueError('Splitting of unsupported event.')
rf_dead_time=100e-6, adc_dead_time=10e-6) # Fat saturation if fatsat_enable: b0 = 1.494 sat_ppm = -3.45 sat_freq = sat_ppm * 1e-6 * b0 * system.gamma rf_fs, _, _ = make_gauss_pulse(flip_angle=110 * math.pi / 180, system=system, duration=8e-3, bandwidth=abs(sat_freq), freq_offset=sat_freq) gz_fs = make_trapezoid(channel='z', system=system, delay=calc_duration(rf_fs), area=1 / 1e-4) # Create 90 degree slice selection pulse and gradient rf, gz, _ = make_sinc_pulse(flip_angle=math.pi / 2, system=system, duration=3e-3, slice_thickness=slice_thickness, apodization=0.5, time_bw_product=4) # Refocusing pulse with spoiling gradients rf180, gz180, _ = make_sinc_pulse(flip_angle=math.pi, system=system, duration=5e-3, slice_thickness=slice_thickness,
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 gradient_waveforms(self) -> np.ndarray: """ Decompress the entire gradient waveform. Returns an array of shape `gradient_axesxtimepoints`. `gradient_axes` is typically 3. Returns ------- grad_waveforms : numpy.ndarray Decompressed gradient waveform. """ duration, num_blocks, _ = self.duration() wave_length = math.ceil(duration / self.grad_raster_time) grad_channels = 3 grad_waveforms = np.zeros((grad_channels, wave_length)) grad_channels = ['gx', 'gy', 'gz'] t0 = 0 t0_n = 0 for i in range(num_blocks): block = self.get_block(i + 1) for j in range(len(grad_channels)): if hasattr(block, grad_channels[j]): grad = getattr(block, grad_channels[j]) if grad.type == 'grad': nt_start = round( (grad.delay + grad.t[0]) / self.grad_raster_time) waveform = grad.waveform else: nt_start = round(grad.delay / self.grad_raster_time) if abs(grad.flat_time) > np.finfo(float).eps: t = np.cumsum([ 0, grad.rise_time, grad.flat_time, grad.fall_time ]) trap_form = np.multiply([0, 1, 1, 0], grad.amplitude) else: t = np.cumsum([0, grad.rise_time, grad.fall_time]) trap_form = np.multiply([0, 1, 0], grad.amplitude) tn = math.floor(t[-1] / self.grad_raster_time) t = np.append(t, t[-1] + self.grad_raster_time) trap_form = np.append(trap_form, 0) if abs(grad.amplitude) > np.finfo(float).eps: waveform = points_to_waveform( t, trap_form, self.grad_raster_time) else: waveform = np.zeros(tn + 1) if waveform.size != np.sum(np.isfinite(waveform)): raise Warning( 'Not all elements of the generated waveform are finite' ) """ Matlab dynamically resizes arrays during slice assignment operation if assignment is out of bounds Numpy does not Following lines are a workaround """ l1, l2 = int(t0_n + nt_start), int(t0_n + nt_start + max(waveform.shape)) if l2 > grad_waveforms.shape[1]: grad_waveforms.resize((len(grad_channels), l2)) grad_waveforms[j, l1:l2] = waveform t0 += calc_duration(block) t0_n = round(t0 / self.grad_raster_time) return grad_waveforms
def align( **kwargs: Union[SimpleNamespace, List[SimpleNamespace]] ) -> List[SimpleNamespace]: """ Aligns `SimpleNamespace` objects as per specified alignment options by setting delays of the pulse sequence events within the block. All previously configured delays within objects are taken into account during calculating of the block duration but then reset according to the selected alignment. Possible values for align_spec are 'left', 'center', 'right'. Parameters ---------- args : dict[str, list[SimpleNamespace] Dictionary mapping of alignment options and `SimpleNamespace` objects. Template: alignment_spec1=SimpleNamespace, alignment_spec2=[SimpleNamespace, ...], ... Alignment spec must be one of `left`, `center` or `right`. Returns ------- objects : list List of aligned `SimpleNamespace` objects. Raises ------ ValueError If first parameter is not of type `str`. If invalid alignment spec is passed. Must be one of `left`, `center` or `right`. """ alignment_specs = list(kwargs.keys()) if not isinstance(alignment_specs[0], str): raise ValueError( f'First parameter must be of type str. Passed: {type(alignment_specs[0])}' ) alignment_options = ['left', 'center', 'right'] if np.any( [align_opt not in alignment_options for align_opt in alignment_specs]): raise ValueError('Invalid alignment spec.') alignments = [] objects = [] for a in alignment_specs: objects_to_align = kwargs[a] a = alignment_options.index(a) if isinstance(objects_to_align, (list, np.ndarray, tuple)): alignments.extend([a] * len(objects_to_align)) objects.extend(objects_to_align) elif isinstance(objects_to_align, SimpleNamespace): alignments.extend([a]) objects.append(objects_to_align) dur = calc_duration(*objects) for i in range(len(objects)): if alignments[i] == 0: objects[i].delay = 0 elif alignments[i] == 1: objects[i].delay = (dur - calc_duration(objects[i])) / 2 elif alignments[i] == 2: objects[i].delay = dur - calc_duration( objects[i]) + objects[i].delay return objects
def bssfp_readout(seq, system, fov=200e-3, Nstartup=11, Ny=128): """ Creates a Balanced steady-state free precession (bSSFP) sequence and adds to seq object Parameters ---------- seq: object Sequence object system : Opts System limits. fov : float, optional Field-of-view [m]. Default is 0.2 m. Nstartup : float, optional Number of start-up RF pulses. Default is 11 Ny : float, optional Number of phase encoding lines. Default is 128. Returns ------- seq : SimpleNamespace Seq object with bSSFP readout TR : float Repetition time Ny : float Final number of phase encoding lines. """ # Sequence Parameters enc = 'xyz' Nx = 128 thk = 6e-3 fa = 35 # [deg] Nramp = Nstartup # ADC duration (controls TR/TE) adc_dur = 2560 / 2 # [us] rf_dur = 490 # [us] rf_apo = 0.5 rf_bwt = 1.5 ############################################################################# # Create slice selection pulse and gradient rf, g_ss, __ = make_sinc_pulse( flip_angle=fa * pi / 180, system=system, duration=rf_dur * 1e-6, slice_thickness=thk, apodization=rf_apo, time_bw_product=rf_bwt ) # for next pyPulseq version add:, return_gz=True) g_ss.channel = enc[2] # Slice refocusing g_ss_reph = make_trapezoid(channel=enc[2], system=system, area=-g_ss.area / 2, duration=0.00017 * 2) ############################################################################# rf.delay = calc_duration(g_ss) - calc_duration(rf) + rf.delay ############################################################################# # Readout gradient and ADC delta_k = 1 / fov kWidth = Nx * delta_k # Readout and ADC g_ro = make_trapezoid(channel=enc[0], system=system, flat_area=kWidth, flat_time=(adc_dur) * 1e-6) adc = make_adc(num_samples=Nx, duration=g_ro.flat_time, delay=g_ro.rise_time) # Readout rewinder g_ro_pre = make_trapezoid(channel=enc[0], system=system, area=-g_ro.area / 2) phaseAreas_tmp = np.arange(0, Ny, 1).tolist() phaseAreas = np.dot(np.subtract(phaseAreas_tmp, Ny / 2), delta_k) gs8_times = [ 0, g_ro.fall_time, g_ro.fall_time + g_ro_pre.rise_time, g_ro.fall_time + g_ro_pre.rise_time + g_ro_pre.flat_time, g_ro.fall_time + g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time ] gs8_amp = [g_ro.amplitude, 0, g_ro_pre.amplitude, g_ro_pre.amplitude, 0] gx_2 = make_extended_trapezoid(channel=enc[0], times=gs8_times, amplitudes=gs8_amp) # Calculate phase encoding gradient duration pe_dur = calc_duration(gx_2) gx_allExt_times = [ 0, g_ro_pre.rise_time, g_ro_pre.rise_time + g_ro_pre.flat_time, g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time, g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time + g_ro.rise_time, g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time + g_ro.rise_time + g_ro.flat_time + 1e-5, g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time + g_ro.rise_time + g_ro.flat_time + 1e-5 + g_ro.fall_time, g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time + g_ro.rise_time + g_ro.flat_time + 1e-5 + g_ro.fall_time + g_ro_pre.rise_time, g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time + g_ro.rise_time + g_ro.flat_time + 1e-5 + g_ro.fall_time + g_ro_pre.rise_time + g_ro_pre.flat_time, g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time + g_ro.rise_time + g_ro.flat_time + 1e-5 + g_ro.fall_time + g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time ] gx_allExt_amp = [ 0, g_ro_pre.amplitude, g_ro_pre.amplitude, 0, g_ro.amplitude, g_ro.amplitude, 0, g_ro_pre.amplitude, g_ro_pre.amplitude, 0 ] gx_all = make_extended_trapezoid(channel=enc[0], times=gx_allExt_times, amplitudes=gx_allExt_amp) ############################################################################# gzrep_times = [ 0, g_ss_reph.rise_time, g_ss_reph.rise_time + g_ss_reph.flat_time, g_ss_reph.rise_time + g_ss_reph.flat_time + g_ss_reph.fall_time, g_ss_reph.rise_time + g_ss_reph.flat_time + g_ss_reph.fall_time + calc_duration(g_ro) + 2 * calc_duration(g_ro_pre) - 2 * calc_duration(g_ss_reph) + 1e-5, g_ss_reph.rise_time + g_ss_reph.flat_time + g_ss_reph.fall_time + calc_duration(g_ro) + 2 * calc_duration(g_ro_pre) - 2 * calc_duration(g_ss_reph) + 1e-5 + g_ss_reph.rise_time, g_ss_reph.rise_time + g_ss_reph.flat_time + g_ss_reph.fall_time + calc_duration(g_ro) + 2 * calc_duration(g_ro_pre) - 2 * calc_duration(g_ss_reph) + 1e-5 + g_ss_reph.rise_time + g_ss_reph.flat_time, g_ss_reph.rise_time + g_ss_reph.flat_time + g_ss_reph.fall_time + calc_duration(g_ro) + 2 * calc_duration(g_ro_pre) - 2 * calc_duration(g_ss_reph) + 1e-5 + g_ss_reph.rise_time + g_ss_reph.flat_time + g_ss_reph.fall_time ] gzrep_amp = [ 0, g_ss_reph.amplitude, g_ss_reph.amplitude, 0, 0, g_ss_reph.amplitude, g_ss_reph.amplitude, 0 ] gzrep_all = make_extended_trapezoid(channel=enc[2], times=gzrep_times, amplitudes=gzrep_amp) adc.delay = g_ro_pre.rise_time + g_ro_pre.flat_time + g_ro_pre.fall_time + g_ro.rise_time + 0.5e-5 # finish timing calculation TR = calc_duration(g_ss) + calc_duration(gx_all) TE = TR / 2 ni_acqu_pattern = np.arange(1, Ny + 1, 1) Ny_aq = len(ni_acqu_pattern) print('Acquisition window is: %3.2f ms' % (TR * Ny_aq * 1e3)) rf05 = rf rf_waveform = rf.signal ############################################################################ # Start-up RF pulses # (ramp-up of Nramp) for nRamp in np.arange(1, Nramp + 1, 1): if np.mod(nRamp, 2): rf.phase_offset = 0 adc.phase_offset = 0 else: rf.phase_offset = -pi adc.phase_offset = -pi rf05.signal = np.divide(nRamp, Nramp) * rf_waveform gyPre_2 = make_trapezoid('y', area=phaseAreas[0], duration=pe_dur, system=system) gyPre_1 = make_trapezoid('y', area=-phaseAreas[0], duration=pe_dur, system=system) gyPre_times = [ 0, gyPre_2.rise_time, gyPre_2.rise_time + gyPre_2.flat_time, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time + g_ro.flat_time + 1e-5, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time + g_ro.flat_time + 1e-5 + gyPre_1.rise_time, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time + g_ro.flat_time + 1e-5 + gyPre_1.rise_time + gyPre_1.flat_time, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time + g_ro.flat_time + 1e-5 + gyPre_1.rise_time + gyPre_1.flat_time + gyPre_1.fall_time ] gyPre_amp = [ 0, gyPre_2.amplitude, gyPre_2.amplitude, 0, 0, gyPre_1.amplitude, gyPre_1.amplitude, 0 ] gyPre_all = make_extended_trapezoid(channel=enc[1], times=gyPre_times, amplitudes=gyPre_amp) if nRamp == 1: seq.add_block(rf05, g_ss) seq.add_block(gx_all, gyPre_all, gzrep_all) else: seq.add_block(rf05, g_ss) seq.add_block(gx_all, gyPre_all, gzrep_all) ############################################################################ ############################################################################ # Actual Readout iterate number of phase encoding steps Ny for i in np.arange(1, Ny + 1, 1): # ******************************* # Phase cycling if np.mod(i + Nramp, 2): rf.phase_offset = 0 adc.phase_offset = 0 else: rf.phase_offset = -pi adc.phase_offset = -pi # ******************************* # *************************************************************************************** # Create phase encoding gradients if np.mod(Nramp, 2): gyPre_2 = make_trapezoid('y', area=phaseAreas[i - 1], duration=pe_dur, system=system) if i > 1: gyPre_1 = make_trapezoid( 'y', area=-phaseAreas[np.mod(i + Ny - 2, Ny)], duration=pe_dur, system=system) else: gyPre_1 = make_trapezoid( 'y', area=-phaseAreas[np.mod(i + Ny - 1, Ny)], duration=pe_dur, system=system) else: gyPre_2 = make_trapezoid('y', area=phaseAreas[i], duration=pe_dur, system=system) if i > 1: gyPre_1 = make_trapezoid( 'y', area=-phaseAreas[np.mod(i + Ny - 2, Ny)], duration=pe_dur, system=system) else: gyPre_1 = make_trapezoid('y', area=phaseAreas[np.mod( i + Ny - 2, Ny)], duration=pe_dur, system=system) gyPre_times = [ 0, gyPre_2.rise_time, gyPre_2.rise_time + gyPre_2.flat_time, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time + g_ro.flat_time + 1e-5, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time + g_ro.flat_time + 1e-5 + gyPre_2.rise_time, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time + g_ro.flat_time + 1e-5 + gyPre_2.rise_time + gyPre_2.flat_time, gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time + g_ro.flat_time + 1e-5 + gyPre_2.rise_time + gyPre_2.flat_time + gyPre_2.fall_time ] gyPre_amp = [ 0, gyPre_2.amplitude, gyPre_2.amplitude, 0, 0, -gyPre_2.amplitude, -gyPre_2.amplitude, 0 ] # Verify if phase encoding gradient amplitude is not zero try: gyPre_all = make_extended_trapezoid(channel=enc[1], times=gyPre_times, amplitudes=gyPre_amp) flag_zerogy = 0 except: gyPre_times = [ 0, pe_dur, pe_dur + g_ro.flat_time + 1e-5, pe_dur + g_ro.flat_time + 1e-5 + pe_dur ] gyPre_amp = [0, gyPre_2.amplitude, 0, 0] flag_zerogy = 1 # *************************************************************************************** # add RF and Slice selecitve gradient seq.add_block(rf, g_ss) # add Phase and frequency encoding gradients and ADC if flag_zerogy == 1: seq.add_block(gx_all, gzrep_all, adc) else: seq.add_block(gx_all, gyPre_all, gzrep_all, adc) return seq, TR, Ny
# Scheme 5(3)3 NImage_perInv = [5, 3] Nrecover_btwInv = [3] TI_Vector = [ 0.100, 0.100 + 0.08 ] # [s] TI1 minimum TI of 100 ms, TI increment of 80 sec, Messroghli 2007 TI_Vector_real = np.zeros( np.shape(TI_Vector)) # Initialize vector for final TI N_inversion = np.shape(NImage_perInv) # Number of inversions print('Number of inversion:', N_inversion[0]) print('Number of images per inversion:', NImage_perInv) print('Number of recovery heart beats between inversion:', Nrecover_btwInv) # Minimum Inversion Time TI_min_attainable = (Nstartup + Ny / 2) * TR + calc_duration(gzSpoil_INV) print('Minimum TI: %3.0f' % (TI_min_attainable * 1e3), 'ms') # Iterate between Inversions for nInv in np.arange(N_inversion[0]): print('----------- Inversion %3.0f -----------' % (nInv + 1)) if nInv > 0: for nRec in np.arange(Nrecover_btwInv[nInv - 1]): seq.add_block(trig_BetweenInversion) # *************************************************************************************************** # Verify minimum possible TI # (nearest to TI_Vector[0], i.e. 100 ms) try: assert (all(TI_Vector[nInv] >= TI_min_attainable) ) # verify if TI_vector value is possbile
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 plot(self, time_range=(0, np.inf)): """ Show Matplotlib plot of all Events in the Sequence object. Parameters ---------- time_range : List Time range (x-axis limits) for plot to be shown. Default is 0 to infinity (entire plot shown). """ fig1, fig2 = plt.figure(1), plt.figure(2) f11, f12, f13 = fig1.add_subplot(311), fig1.add_subplot( 312), fig1.add_subplot(313) f2 = [ fig2.add_subplot(311), fig2.add_subplot(312), fig2.add_subplot(313) ] 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 block is not None: if 'adc' in block: adc = block['adc'] t = adc.delay + [ (x * adc.dwell) for x in range(0, int(adc.num_samples)) ] f11.plot((t0 + t), np.zeros(len(t))) if 'rf' in block: rf = block['rf'] t = rf.t f12.plot(np.squeeze(t0 + t), abs(rf.signal)) f13.plot(np.squeeze(t0 + t), np.angle(rf.signal)) grad_channels = ['gx', 'gy', 'gz'] for x in range(0, len(grad_channels)): if grad_channels[x] in block: grad = block[grad_channels[x]] if grad.type == 'grad': t = grad.t waveform = 1e-3 * grad.waveform else: t = np.cumsum([ 0, grad.rise_time, grad.flat_time, grad.fall_time ]) waveform = [ 1e-3 * grad.amplitude * x for x in [0, 1, 1, 0] ] f2[x].plot(np.squeeze(t0 + t), waveform) t0 += calc_duration(block) f11.set_ylabel('adc') f12.set_ylabel('rf mag hz') f13.set_ylabel('rf phase rad') [f2[x].set_ylabel(grad_channels[x]) for x in range(3)] # Setting display limits disp_range = [time_range[0], min(t0, time_range[1])] f11.set_xlim(disp_range) f12.set_xlim(disp_range) f13.set_xlim(disp_range) [x.set_xlim(disp_range) for x in f2] 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
} gz_reph = make_trapezoid(kwargs_for_gz_reph) flip = 180 * pi / 180 kwargs_for_sinc = { "flip_angle": flip, "system": system, "duration": 2e-3, "slice_thickness": slice_thickness, "apodization": 0.5, "time_bw_product": 4 } rf180, gz180 = make_sinc_pulse(kwargs_for_sinc, 2) TE, TR = 100e-3, 4408e-3 delayTE1 = TE / 2 - calc_duration(gz_reph) - calc_duration( rf) - calc_duration(rf180) / 2 delayTE2 = TE / 2 - calc_duration(gx) / 2 - calc_duration(rf180) / 2 delayTE3 = TR - TE - calc_duration(gx) delay1 = make_delay(delayTE1) delay2 = make_delay(delayTE2) delay3 = make_delay(delayTE3) for i in range(2): seq.add_block(rf, gz) kwargs_for_gy_pre = { "channel": 'y', "system": system, "area": -(Ny / 2 - i) * delta_k, "duration": readoutTime / 2 }
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
rf_dur *= 1e-3 # [s] slice_res *= 1e-3 # [m] if Nintl/redfac%1 != 0: raise ValueError('Number of interleaves is not multiple of reduction factor') if spiraltype!=1 and spiraltype!=4: ValueError('Right now only spiraltype 1 (spiral out) and 4 (ROI) possible.') #%% RF Pulse and slab/slice selection gradient # make rf pulse and calculate duration of excitation and rewinding rf, gz, gz_rew, rf_del = make_sinc_pulse(flip_angle=flip_angle*np.pi/180, system=system, duration=rf_dur, slice_thickness=slice_res, apodization=0.5, time_bw_product=tbp_exc, use='excitation', return_gz=True, return_delay=True) exc_to_rew = rf_del.delay - rf_dur/2 - rf.delay # time from middle of rf pulse to rewinder, rf_del.delay equals the block length rew_dur = calc_duration(gz_rew) # RF spoiling parameters rf_spoiling_inc = 50 # increment of RF spoiling [°] rf_phase = 0 rf_inc = 0 # Fat saturation if fatsat: if B0 > 4: fatsat_bw = 1000 # bandwidth of fatsat pulse [Hz] else: fatsat_bw = 300 fatsat_fa = 110 # flip angle [°] fatsat_dur = ph.round_up_to_raster(fatsat_tbp/fatsat_bw, decimals=5)
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()
"system": system, "area": -gx.area / 2, "duration": 2e-3 } gx_pre = make_trapezoid(kwargs_for_gxpre) kwargs_for_gz_reph = { "channel": 'z', "system": system, "area": -gz.area / 2, "duration": 2e-3 } gz_reph = make_trapezoid(kwargs_for_gz_reph) phase_areas = (np.arange(Ny) - (Ny / 2)) * delta_k TE, TR = 10e-3, 200e-3 delayTE = TE - calc_duration( gx_pre) - calc_duration(gz) / 2 - calc_duration(gx) / 2 delayTR = TR - calc_duration(gx_pre) - calc_duration(gz) - calc_duration( gx) - delayTE delay1 = make_delay(delayTE) delay2 = make_delay(delayTR) for i in range(Ny): seq.add_block(rf, gz) kwargsForGyPre = { "channel": 'y', "system": system, "area": phase_areas[i], "duration": 2e-3 } gyPre = make_trapezoid(kwargsForGyPre) seq.add_block(gx_pre, gyPre, gz_reph)
sys = Opts(max_grad=28, grad_unit='mT/m', max_slew=150, slew_unit='T/m/s', rf_ringdown_time=20e-6, rf_dead_time=100e-6, adc_dead_time=10e-6) rf, gz, gzr = make_sinc_pulse(flip_angle=alpha * math.pi / 180, duration=4e-3, slice_thickness=slice_thickness, apodization=0.5, time_bw_product=4, system=sys) deltak = 1 / fov gx = make_trapezoid(channel='x', flat_area=Nx * deltak, flat_time=6.4e-3, system=sys) adc = make_adc(num_samples=Nx, duration=gx.flat_time, delay=gx.rise_time, system=sys) gx_pre = make_trapezoid(channel='x', area=-gx.area / 2, duration=2e-3, system=sys) gz_reph = make_trapezoid(channel='z', area=-gz.area / 2, duration=2e-3, system=sys) phase_areas = (np.arange(Ny) - Ny / 2) * deltak gx_spoil = make_trapezoid(channel='x', area=2 * Nx * deltak, system=sys) gz_spoil = make_trapezoid(channel='z', area=4 / slice_thickness, system=sys) delay_TE = math.ceil((TE - calc_duration(gx_pre) - gz.fall_time - gz.flat_time / 2 - calc_duration( gx) / 2) / seq.grad_raster_time) * seq.grad_raster_time delay_TR = math.ceil((TR - calc_duration(gx_pre) - calc_duration(gz) - calc_duration( gx) - delay_TE) / seq.grad_raster_time) * seq.grad_raster_time if not np.all(delay_TR >= calc_duration(gx_spoil, gz_spoil)): raise Exception() rf_phase = 0 rf_inc = 0 for i in range(Ny): rf.phase_offset = rf_phase / 180 * np.pi adc.phase_offset = rf_phase / 180 * np.pi rf_inc = divmod(rf_inc + rf_spoiling_inc, 360.0)[1] rf_phase = divmod(rf_phase + rf_inc, 360.0)[1]
def add_gradients(grads: Union[list, tuple], system=Opts(), max_grad: int = 0, max_slew: int = 0) -> SimpleNamespace: """ Superpose several gradient events. Parameters ---------- grads : list or tuple List or tuple of 'SimpleNamespace' gradient events. system : Opts, optional, default=Opts() System limits. max_grad : float, optional, default=0 Maximum gradient amplitude. max_slew : float, optional, default=0 Maximum slew rate. Returns ------- grad : SimpleNamespace Superimposition of gradient events from `grads`. """ max_grad = max_grad if max_grad > 0 else system.max_grad max_slew = max_slew if max_slew > 0 else system.max_slew if len(grads) < 2: raise Exception() # First gradient defines channel channel = grads[0].channel # Find out the general delay of all gradients and other statistics delays, firsts, lasts, durs = [], [], [], [] for ii in range(len(grads)): delays.append(grads[ii].delay) firsts.append(grads[ii].first) lasts.append(grads[ii].last) durs.append(calc_duration(grads[ii])) # Convert to numpy.ndarray for fancy-indexing later on firsts, lasts = np.array(firsts), np.array(lasts) common_delay = min(delays) total_duration = max(durs) waveforms = dict() max_length = 0 for ii in range(len(grads)): g = grads[ii] if g.type == 'grad': waveforms[ii] = g.waveform elif g.type == 'trap': if g.flat_time > 0: # Triangle or trapezoid times = [ g.delay - common_delay, g.delay - common_delay + g.rise_time, g.delay - common_delay + g.rise_time + g.flat_time, g.delay - common_delay + g.rise_time + g.flat_time + g.fall_time ] amplitudes = [0, g.amplitude, g.amplitude, 0] else: times = [ g.delay - common_delay, g.delay - common_delay + g.rise_time, g.delay - common_delay + g.rise_time + g.flat_time ] amplitudes = [0, g.amplitude, 0] waveforms[ii] = points_to_waveform( times=times, amplitudes=amplitudes, grad_raster_time=system.grad_raster_time) else: raise ValueError('Unknown gradient type') if g.delay - common_delay > 0: # Stop for numpy.arange is not g.delay - common_delay - system.grad_raster_time like in Matlab # so as to include the endpoint t_delay = np.arange(0, g.delay - common_delay, step=system.grad_raster_time) waveforms[ii] = np.insert(waveforms[ii], 0, t_delay) num_points = len(waveforms[ii]) max_length = num_points if num_points > max_length else max_length w = np.zeros(max_length) for ii in range(len(grads)): wt = np.zeros(max_length) wt[0:len(waveforms[ii])] = waveforms[ii] w += wt grad = make_arbitrary_grad(channel, w, system, max_slew=max_slew, max_grad=max_grad, delay=common_delay) grad.first = np.sum(firsts[np.array(delays) == common_delay]) grad.last = np.sum(lasts[np.where(durs == total_duration)]) return grad
def add_block(self, block_index: int, *args): """ Inserts pulse sequence events into `self.block_events` at position `block_index`. Also performs gradient checks. Parameters ---------- block_index : int Index at which `SimpleNamespace` events have to be inserted into `self.block_events`. args : list List of `SimpleNamespace` pulse sequence events to be added to `self.block_events`. """ block_duration = calc_duration(*args) self.block_events[block_index] = np.zeros(6, dtype=np.int) duration = 0 check_g = {} for event in args: if event.type == 'rf': mag = np.abs(event.signal) amplitude = np.max(mag) mag = np.divide(mag, amplitude) # Following line of code is a workaround for numpy's divide functions returning NaN when mathematical # edge cases are encountered (eg. divide by 0) mag[np.isnan(mag)] = 0 phase = np.angle(event.signal) phase[phase < 0] += 2 * np.pi phase /= 2 * np.pi mag_shape = compress_shape(mag) data = np.insert(mag_shape.data, 0, mag_shape.num_samples) mag_id, found = self.shape_library.find(data) if not found: self.shape_library.insert(mag_id, data) phase_shape = compress_shape(phase) data = np.insert(phase_shape.data, 0, phase_shape.num_samples) phase_id, found = self.shape_library.find(data) if not found: self.shape_library.insert(phase_id, data) use = 0 use_cases = {'excitation': 1, 'refocusing': 2, 'inversion': 3} if hasattr(event, 'use'): use = use_cases[event.use] data = np.array((amplitude, mag_id, phase_id, event.delay, event.freq_offset, event.phase_offset, event.dead_time, event.ringdown_time, use)) data_id, found = self.rf_library.find(data) if not found: self.rf_library.insert(data_id, data) self.block_events[block_index][1] = data_id duration = max( duration, max(mag.shape) * self.rf_raster_time + event.dead_time + event.ringdown_time + event.delay) elif event.type == 'grad': channel_num = ['x', 'y', 'z'].index(event.channel) idx = 2 + channel_num check_g[channel_num] = SimpleNamespace() check_g[channel_num].idx = idx check_g[channel_num].start = np.array( (event.delay + min(event.t), event.first)) check_g[channel_num].stop = np.array( (event.delay + max(event.t) + self.system.grad_raster_time, event.last)) amplitude = max(abs(event.waveform)) if amplitude > 0: g = event.waveform / amplitude else: g = event.waveform shape = compress_shape(g) data = np.insert(shape.data, 0, shape.num_samples) shape_id, found = self.shape_library.find(data) if not found: self.shape_library.insert(shape_id, data) data = np.array( [amplitude, shape_id, event.delay, event.first, event.last]) grad_id, found = self.grad_library.find(data) if not found: self.grad_library.insert(grad_id, data, 'g') idx = 2 + channel_num self.block_events[block_index][idx] = grad_id duration = max(duration, len(g) * self.grad_raster_time) elif event.type == 'trap': channel_num = ['x', 'y', 'z'].index(event.channel) idx = 2 + channel_num check_g[channel_num] = SimpleNamespace() check_g[channel_num].idx = idx check_g[channel_num].start = np.array((0, 0)) check_g[channel_num].stop = np.array( (event.delay + event.rise_time + event.fall_time + event.flat_time, 0)) data = np.array([ event.amplitude, event.rise_time, event.flat_time, event.fall_time, event.delay ]) trap_id, found = self.grad_library.find(data) if not found: self.grad_library.insert(trap_id, data, 't') self.block_events[block_index][idx] = trap_id duration = max( duration, event.delay + event.rise_time + event.flat_time + event.fall_time) elif event.type == 'adc': data = np.array([ event.num_samples, event.dwell, max(event.delay, event.dead_time), event.freq_offset, event.phase_offset, event.dead_time ]) adc_id, found = self.adc_library.find(data) if not found: self.adc_library.insert(adc_id, data) self.block_events[block_index][5] = adc_id duration = max( duration, event.delay + event.num_samples * event.dwell + event.dead_time) elif event.type == 'delay': data = np.array([event.delay]) delay_id, found = self.delay_library.find(data) if not found: self.delay_library.insert(delay_id, data) self.block_events[block_index][0] = delay_id duration = max(duration, event.delay) # ========= # PERFORM GRADIENT CHECKS # ========= for cg_temp in check_g.keys(): cg = check_g[cg_temp] if abs(cg.start[1] ) > self.system.max_slew * self.system.grad_raster_time: if cg.start[0] != 0: raise ValueError( 'No delay allowed for gradients which start with a non-zero amplitude' ) if block_index > 1: prev_id = self.block_events[block_index - 1][cg.idx] if prev_id != 0: prev_lib = self.grad_library.get(prev_id) prev_dat = prev_lib['data'] prev_type = prev_lib['type'] if prev_type == 't': raise Exception( 'Two consecutive gradients need to have the same amplitude at the connection point' ) elif prev_type == 'g': last = prev_dat[4] if abs( last - cg.start[1] ) > self.system.max_slew * self.system.grad_raster_time: raise Exception( 'Two consecutive gradients need to have the same amplitude at the connection point' ) else: raise Exception( 'First gradient in the the first block has to start at 0.') if cg.stop[ 1] > self.system.max_slew * self.system.grad_raster_time and abs( cg.stop[0] - block_duration) > 1e-7: raise Exception( 'A gradient that doesnt end at zero needs to be aligned to the block boundary.' )
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