Skip to content

Commit 3716a90

Browse files
committed
feat: add support for 2D GRE
1 parent ac9aee4 commit 3716a90

File tree

1 file changed

+96
-33
lines changed

1 file changed

+96
-33
lines changed

src/mrinufft/io/pulseq.py

Lines changed: 96 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,37 @@
1515
from 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

Comments
 (0)