1515from mrinufft .trajectories .utils import KMAX
1616
1717
18- def read_pulseq_traj (seq , traj_delay = 0.0 , grad_offset = 0.0 ):
18+ def read_pulseq_traj (seq , trajectory_delay = 0.0 , gradient_offset = 0.0 ):
1919 """Extract k-space trajectory from a Pulseq sequence file.
2020
2121 The sequence should be a valid Pulseq `.seq` file, with arbitrary gradient
2222 waveforms, which all have the same length.
2323
24- Note
2524
2625 Parameters
2726 ----------
2827 sequence : pulseq Sequence, or Path to the Pulseq `.seq` file.
29-
28+ The Pulseq sequence object or the path to the `.seq` file.
29+ trajectory_delay : float, optional
30+ Compensation factor in seconds (s) to align ADC and gradients in the reconstruction.
31+ gradient_offset : float, optional
32+ Simulates background gradients (specified in Hz/m)
3033 Returns
3134 -------
3235 description : dict
3336 the [DESCRIPTION] block from the sequence file.
3437 np.ndarray
3538 The k-space trajectory as a numpy array of shape (n_shots, n_samples, 3),
3639 where the last dimension corresponds to the x, y, and z coordinates in k-space.
37-
3840 """
3941 if not isinstance (seq , pp .Sequence ):
4042 filename = seq
4143 seq = pp .Sequence ()
4244 seq .read (filename )
4345
44- kspace_adc , _ , t_exc , t_refocus , t_adc = seq .calculate_kspace ()
46+ kspace_adc , _ , t_exc , t_refocus , t_adc = seq .calculate_kspace (
47+ trajectory_delay = trajectory_delay , gradient_offset = gradient_offset
48+ )
4549
4650 if not t_adc :
4751 raise ValueError (
@@ -77,18 +81,20 @@ def _check_timings(seq):
7781 warnings .warn ("Timing check failed. Error listing follows:" + str (error_report ))
7882
7983
80- def pulseq_gre_3D (
84+ def pulseq_gre (
8185 trajectory : NDArray ,
8286 fov : tuple [float , float , float ],
8387 img_size : tuple [int , int , int ],
8488 TR : float ,
8589 TE : float ,
8690 TE_pos : float = 0.5 ,
8791 FA : float | None = None ,
92+ gre_2D : bool = False ,
93+ slice_overlap : float = 0.0 ,
8894 rf_pulse : SimpleNamespace | None = None ,
8995 rf_spoiling_inc : float = 0.0 ,
9096 grad_spoil_factor : float = 2.0 ,
91- system : pp .Opts = pp . Opts . default ,
97+ system : pp .Opts | None = None ,
9298 osf : int = 1 ,
9399) -> pp .Sequence :
94100 """Create a Pulseq 3D-GRE sequence for arbitrary trajectories.
@@ -114,7 +120,12 @@ def pulseq_gre_3D(
114120 rf_spoiling_inc: float, optional
115121 The increment in the RF phase (in degree) for spoiling. Default is 0.0,
116122 which means no spoiling.
117-
123+ gre_2D: bool, optional
124+ If True, the sequence will be a 2D GRE sequence. Default is False,
125+ which means a 3D GRE sequence.
126+ slice_overlap: float, optional
127+ The slice overlap proportion for 2D GRE sequences. Default is 0.0
128+ Positive values indicates an overlap, negative values indicates a gap.
118129 grad_spoil_factor: float, optional
119130 The factor by which the spoiler gradient moves to the edge of k-space. Default is 2.0.
120131 osf: int, optional
@@ -137,16 +148,27 @@ def pulseq_gre_3D(
137148 4. Gradient spoilers
138149 5. Delay to sync the next TR
139150
151+
152+ If `gre_2D` is True, the sequence will be a 2D GRE sequence, and the slice
153+ thickness will be determined as :math:`(FOV[2] / img_size[2]) *
154+ (1+slice_overlap)`
155+
140156 Returns
141157 -------
142158 pp.Sequence
143159 A Pulseq sequence object with the specified arbitrary gradient waveform.
144160 """
145161 TR = TR / 1000.0 # convert to seconds
146162 TE = TE / 1000.0 # convert to seconds
163+ if system is None :
164+ system = pp .Opts .default
147165 seq = pp .Sequence (system = system )
148166
149-
167+ if gre_2D and rf_pulse is not None :
168+ raise ValueError (
169+ "2D GRE sequences do not support custom RF pulses. "
170+ "Please set `rf_pulse` to None or use a block pulse."
171+ )
150172 if rf_pulse is None and FA is not None :
151173 rf_pulse = pp .make_block_pulse (flip_angle = FA , system = system , use = "excitation" )
152174 elif rf_pulse is not None and FA is not None :
@@ -164,7 +186,9 @@ def pulseq_gre_3D(
164186 if FA is None :
165187 # Compute the flip angle from the RF pulse
166188 # following https://github.com/imr-framework/pypulseq/tree/src/pypulseq/Sequence/ext_test_report.py#L24
167- FA = float (abs (np .sum (rf_pulse .signal [:- 1 ]* (rf_pulse .t [1 :] - rf_pulse .t [:- 1 ]))) * 360 )
189+ FA = float (
190+ abs (np .sum (rf_pulse .signal [:- 1 ] * (rf_pulse .t [1 :] - rf_pulse .t [:- 1 ]))) * 360
191+ )
168192
169193 seq .set_definition ("FOV" , fov )
170194 seq .set_definition ("ImgSize" , img_size )
@@ -211,6 +235,18 @@ def pulseq_gre_3D(
211235 - delay_before_grad .delay
212236 - full_grads .shape [1 ] * system .grad_raster_time
213237 )
238+ if gre_2D :
239+ return _pulseq_gre_2D (
240+ seq ,
241+ full_grads [:, :2 ],
242+ rf_spoiling_inc ,
243+ adc ,
244+ spoiler ,
245+ delay_before_grad ,
246+ delay_end_TR ,
247+ thickness = fov [2 ] / img_size [2 ] * (1 + slice_overlap ),
248+ slice_locs = trajectory [:,2 ]
249+ )
214250
215251 rf_phase = 0.0
216252 for grad_xyz in full_grads :
@@ -237,28 +273,55 @@ def pulseq_gre_3D(
237273 return seq
238274
239275
240- def gre_2D (trajectory , TR , TE , FA , pulse , system = pp .Opts .default ):
241- """Create a Pulseq 2D-GRE sequence for arbitrary 2D trajectories.
276+ def _pulseq_gre_2D (
277+ seq : pp .Sequence ,
278+ full_grads : NDArray ,
279+ rf_spoiling_inc : float ,
280+ adc : SimpleNamespace ,
281+ spoiler : SimpleNamespace ,
282+ delay_before_grad : SimpleNamespace ,
283+ delay_end_TR : SimpleNamespace ,
284+ thickness : float ,
285+ slice_locs : NDArray
286+ ) -> pp .Sequence :
287+ """Create a Pulseq 2D-GRE sequence for arbitrary trajectories."""
242288
243- Parameters
244- ----------
245- trajectory : np.ndarray
246- The k-space trajectory as a numpy array of shape (n_shots, n_samples, 3),
247- where the last dimension corresponds to the x, y, and z coordinates in k-space.
248- TR: float
249- The repetition time in milliseconds (ms).
250- TE: float
251- The echo time in milliseconds (ms).
252- FA: float
253- The flip angle in degrees (°).
254- pulse: SimpleNamespace
255- A custom radio-frequency pulse object.
256- system : pypulseq.Opts, optional
257- The system options for the Pulseq sequence. Default is `pp.Opts.default`.
289+ rf , gz , gzr = pp .make_sinc_pulse (
290+ flip_angle = float (seq .get_definition ("FA" )),
291+ slice_thickness = thickness ,
292+ return_gz = True ,
293+ system = seq .system ,
294+ ) # type: ignore[assignment]
258295
259- Returns
260- -------
261- pp.Sequence
262- A Pulseq sequence object with the specified arbitrary gradient waveform.
263- """
264- ...
296+ rf_phase = 0.0
297+ nz = seq .get_definition ("ImgSize" )[2 ]
298+ for grad_xyz , kz in zip (full_grads , slice_locs ):
299+ rf_phase = divmod (rf_phase + rf_spoiling_inc , 360.0 )[1 ]
300+
301+ # Update the Gz component
302+ # Update the RF pulse phase offset
303+ rf .frequency_offset = gz .amplitude * thickness * kz * nz
304+ rf .phase_offset = rf_phase / 180 * np .pi
305+ adc .phase_offset = rf_phase / 180 * np .pi
306+ seq .add_block (rf , gz ) # RF pulse and slice selection gradient for a 2D
307+ seq .add_block (adc ) # ADC
308+ seq .add_block (delay_before_grad ) # delay to sync TE
309+ # Add the gradient waveform, the first/last points are set to 0
310+ # to avoid accumulating offsets between shots
311+ seq .add_block (
312+ * [
313+ pp .make_arbitrary_grad (
314+ channel = c ,
315+ waveform = grad_xyz [:, i ],
316+ first = 0 ,
317+ last = 0 ,
318+ system = seq .system ,
319+ )
320+ for i , c in enumerate ("xyz" )
321+ ], # Gx and Gy are arbitrary gradients
322+ gzr , # Refocusing gradient for Gzr
323+ )
324+ seq .add_block (spoiler , delay_end_TR )
325+
326+ _check_timings (seq )
327+ return seq
0 commit comments