Skip to content

Commit 5dba80c

Browse files
authored
Merge pull request #269 from chaithyagr/update_traj_specs
Add support for prephasors, re-winders with timing etc
2 parents 4039070 + f5984e2 commit 5dba80c

File tree

5 files changed

+467
-39
lines changed

5 files changed

+467
-39
lines changed

docs/trajectory_gradspec.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ The binary file format is specified as follows:
3232
+----------------+-------+---------+---------+------------------------------------------------------------------------+
3333
| timestamp | FLOAT | 1 | n.a. | Time stamp when the binary is created |
3434
+----------------+-------+---------+---------+------------------------------------------------------------------------+
35-
| Empty places | FLOAT | 9 | n.a. | Yet unused : Default initialized with 0 |
35+
| ADC pre-skip | FLOAT | 1 | n.a. | Gradient samples to skip before starting ADC, for pre-phasors |
36+
+----------------+-------+---------+---------+------------------------------------------------------------------------+
37+
| ADC post-skip | FLOAT | 1 | n.a. | Gradient samples to skip at the end of trajectory by turning off ADC |
38+
+----------------+-------+---------+---------+------------------------------------------------------------------------+
39+
| Empty places | FLOAT | 7 | n.a. | Yet unused : Default initialized with 0 |
3640
+----------------+-------+---------+---------+------------------------------------------------------------------------+
3741
| kStarts | FLOAT | D*Nc | 1/m | K-space location start |
3842
+----------------+-------+---------+---------+------------------------------------------------------------------------+

src/mrinufft/io/nsp.py

Lines changed: 119 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
Gammas,
1818
check_hardware_constraints,
1919
convert_gradients_to_slew_rates,
20+
unnormalize_trajectory,
2021
convert_trajectory_to_gradients,
2122
)
23+
from mrinufft.trajectories.tools import get_gradient_amplitudes_to_travel_for_set_time
2224

2325
from .siemens import read_siemens_rawdat
2426

@@ -29,14 +31,16 @@ def write_gradients(
2931
grad_filename: str,
3032
img_size: tuple[int, ...],
3133
FOV: tuple[float, ...],
32-
in_out: bool = True,
34+
TE_pos: float = 0.5,
3335
min_osf: int = 5,
3436
gamma: float = Gammas.HYDROGEN,
3537
version: float = 4.2,
3638
recon_tag: float = 1.1,
3739
timestamp: float | None = None,
3840
keep_txt_file: bool = False,
3941
final_positions: np.ndarray | None = None,
42+
start_skip_samples: int = 0,
43+
end_skip_samples: int = 0,
4044
):
4145
"""Create gradient file from gradients and initial positions.
4246
@@ -52,8 +56,10 @@ def write_gradients(
5256
Image size.
5357
FOV : tuple[float, ...]
5458
Field of view.
55-
in_out : bool, optional
56-
Whether it is In-Out trajectory?, by default True
59+
TE_pos : float, optional
60+
The ratio of trajectory when TE occurs, with 0 as start of
61+
trajectory and 1 as end. By default 0.5, which is the
62+
center of the trajectory (in-out trajectory).
5763
min_osf : int, optional
5864
Minimum oversampling factor needed at ADC, by default 5
5965
gamma : float, optional
@@ -69,6 +75,12 @@ def write_gradients(
6975
binary file, by default False
7076
final_positions : np.ndarray, optional
7177
Final positions. Shape (num_shots, dimension), by default None
78+
start_skip_samples : int, optional
79+
Number of samples to skip in ADC at start of each shot, by default 0
80+
This works only for version >= 5.1.
81+
end_skip_samples : int, optional
82+
Number of samples to skip in ADC at end of each shot, by default 0
83+
This works only for version >= 5.1.
7284
7385
"""
7486
num_shots = gradients.shape[0]
@@ -100,14 +112,12 @@ def write_gradients(
100112
file.write(str(num_shots) + "\n")
101113
file.write(str(num_samples_per_shot) + "\n")
102114
if version >= 4.1:
103-
if not in_out:
115+
if TE_pos == 0:
104116
if np.sum(initial_positions) != 0:
105117
warnings.warn(
106118
"The initial positions are not all zero for center-out trajectory"
107119
)
108-
file.write("0\n")
109-
else:
110-
file.write("0.5\n")
120+
file.write(str(TE_pos) + "\n")
111121
# Write the maximum Gradient
112122
file.write(str(max_grad) + "\n")
113123
# Write recon Pipeline version tag
@@ -119,6 +129,10 @@ def write_gradients(
119129
timestamp = float(datetime.now().timestamp())
120130
file.write(str(timestamp) + "\n")
121131
left_over -= 1
132+
if version >= 5.1:
133+
file.write(str(start_skip_samples) + "\n")
134+
file.write(str(end_skip_samples) + "\n")
135+
left_over -= 2
122136
file.write(str("0\n" * left_over))
123137
# Write all the k0 values
124138
file.write(
@@ -165,7 +179,7 @@ def write_gradients(
165179
os.remove(grad_filename + ".txt")
166180

167181

168-
def _pop_elements(array, num_elements=1, type="float"):
182+
def _pop_elements(array, num_elements=1, type=np.float32):
169183
"""Pop elements from an array.
170184
171185
Parameters
@@ -200,9 +214,12 @@ def write_trajectory(
200214
gamma: float = Gammas.HYDROGEN,
201215
raster_time: float = DEFAULT_RASTER_TIME,
202216
check_constraints: bool = True,
217+
TE_pos: float = 0.5,
203218
gmax: float = DEFAULT_GMAX,
204219
smax: float = DEFAULT_SMAX,
205-
version: float = 5,
220+
pregrad: str | None = "speedup",
221+
postgrad: str | None = "slowdown_to_edge",
222+
version: float = 5.1,
206223
**kwargs,
207224
):
208225
"""Calculate gradients from k-space points and write to file.
@@ -226,10 +243,24 @@ def write_trajectory(
226243
Gradient raster time in ms, by default 0.01
227244
check_constraints : bool, optional
228245
Check scanner constraints, by default True
246+
TE_pos : float, optional
247+
The ratio of trajectory when TE occurs, with 0 as start of
248+
trajectory and 1 as end. By default 0.5, which is the
249+
center of the trajectory (in-out trajectory).
229250
gmax : float, optional
230251
Maximum gradient magnitude in T/m, by default 0.04
231252
smax : float, optional
232253
Maximum slew rate in T/m/ms, by default 0.1
254+
pregrad : str, optional
255+
Pregrad method, by default `prephase`
256+
`prephase` will add a prephase gradient to the start of the trajectory.
257+
postgrad : str, optional
258+
Postgrad method, by default 'slowdown_to_edge'
259+
`slowdown_to_edge` will add a gradient to slow down to the edge of the FOV.
260+
This is useful for sequences needing a spoiler at the end of the trajectory.
261+
However, spoiler is still not added, it is expected that the sequence
262+
handles the spoilers, which can be variable.
263+
`slowdown_to_center` will add a gradient to slow down to the center of the FOV.
233264
version: float, optional
234265
Trajectory versioning, by default 5
235266
kwargs : dict, optional
@@ -245,7 +276,46 @@ def write_trajectory(
245276
gamma=gamma,
246277
get_final_positions=True,
247278
)
248-
279+
Ns_to_skip_at_start = 0
280+
Ns_to_skip_at_end = 0
281+
scan_consts = {
282+
"gamma": gamma,
283+
"gmax": gmax,
284+
"smax": smax,
285+
"raster_time": raster_time,
286+
}
287+
if pregrad == "prephase":
288+
if version < 5.1:
289+
raise ValueError(
290+
"pregrad is only supported for version >= 5.1, "
291+
"please set version to 5.1 or higher."
292+
)
293+
start_gradients = get_gradient_amplitudes_to_travel_for_set_time(
294+
ke=initial_positions,
295+
ge=gradients[:, 0],
296+
**scan_consts,
297+
)
298+
initial_positions = np.zeros_like(initial_positions)
299+
gradients = np.hstack([start_gradients, gradients])
300+
Ns_to_skip_at_start = start_gradients.shape[1]
301+
if postgrad is not None:
302+
if version < 5.1:
303+
raise ValueError(
304+
"postgrad is only supported for version >= 5.1, "
305+
"please set version to 5.1 or higher."
306+
)
307+
edge_locations = np.zeros_like(final_positions)
308+
if postgrad == "slowdown_to_edge":
309+
# Always end at KMax, the spoilers can be handeled by the sequence.
310+
edge_locations[..., 0] = img_size[0] / FOV[0] / 2
311+
end_gradients = get_gradient_amplitudes_to_travel_for_set_time(
312+
ke=edge_locations,
313+
gs=gradients[:, -1],
314+
ks=final_positions,
315+
**scan_consts,
316+
)
317+
gradients = np.hstack([gradients, end_gradients])
318+
Ns_to_skip_at_end = end_gradients.shape[1]
249319
# Check constraints if requested
250320
if check_constraints:
251321
slewrates, _ = convert_gradients_to_slew_rates(gradients, raster_time)
@@ -261,6 +331,15 @@ def write_trajectory(
261331
f"Maximum gradient amplitude: {maxG:.3f} > {gmax:.3f}"
262332
f"Maximum slew rate: {maxS:.3f} > {smax:.3f}"
263333
)
334+
if pregrad != "prephase":
335+
border_slew_rate = gradients[:, 0] / raster_time
336+
if np.any(np.abs(border_slew_rate) > smax):
337+
warnings.warn(
338+
"Slew rate at start of trajectory exceeds maximum slew rate!"
339+
f"Maximum slew rate: {np.max(np.abs(border_slew_rate)):.3f}"
340+
" > {smax:.3f}. Please use prephase gradient to avoid this "
341+
" issue."
342+
)
264343

265344
# Write gradients in file
266345
write_gradients(
@@ -270,16 +349,19 @@ def write_trajectory(
270349
grad_filename=grad_filename,
271350
img_size=img_size,
272351
FOV=FOV,
352+
TE_pos=TE_pos,
273353
gamma=gamma,
274354
version=version,
355+
start_skip_samples=Ns_to_skip_at_start,
356+
end_skip_samples=Ns_to_skip_at_end,
275357
**kwargs,
276358
)
277359

278360

279361
def read_trajectory(
280362
grad_filename: str,
281363
dwell_time: float | str = DEFAULT_RASTER_TIME,
282-
num_adc_samples: int = None,
364+
num_adc_samples: int | None = None,
283365
gamma: Gammas | float = Gammas.HYDROGEN,
284366
raster_time: float = DEFAULT_RASTER_TIME,
285367
read_shots: bool = False,
@@ -331,25 +413,27 @@ def read_trajectory(
331413
if dwell_time == "min_osf":
332414
dwell_time = raster_time / min_osf
333415
(num_shots, num_samples_per_shot), data = _pop_elements(data, 2, type="int")
334-
if num_adc_samples is None:
335-
if read_shots:
336-
num_adc_samples = num_samples_per_shot + 1
337-
else:
338-
num_adc_samples = int(num_samples_per_shot * (raster_time / dwell_time))
339-
if version >= 4.1:
416+
if version > 4:
340417
TE, data = _pop_elements(data)
341418
grad_max, data = _pop_elements(data)
342419
recon_tag, data = _pop_elements(data)
343420
recon_tag = np.around(recon_tag, 2)
344421
left_over = 10
345-
if version >= 4.2:
422+
if version > 4.1:
346423
timestamp, data = _pop_elements(data)
347424
timestamp = datetime.fromtimestamp(float(timestamp))
348425
left_over -= 1
426+
if version > 5:
427+
packed_skips, data = _pop_elements(data, num_elements=2, type="int")
428+
start_skip_samples, end_skip_samples = packed_skips
429+
left_over -= 2
430+
else:
431+
start_skip_samples = 0
432+
end_skip_samples = 0
349433
_, data = _pop_elements(data, left_over)
350434
initial_positions, data = _pop_elements(data, dimension * num_shots)
351435
initial_positions = np.reshape(initial_positions, (num_shots, dimension))
352-
if version >= 5:
436+
if version > 4.5:
353437
final_positions, data = _pop_elements(data, dimension * num_shots)
354438
final_positions = np.reshape(final_positions, (num_shots, dimension))
355439
dwell_time_ns = dwell_time * 1e6
@@ -361,11 +445,23 @@ def read_trajectory(
361445
dimension * num_samples_per_shot * num_shots,
362446
)
363447
gradients = np.reshape(
364-
grad_max * gradients, (num_shots * num_samples_per_shot, dimension)
448+
grad_max * gradients * 1e-3, (num_shots, num_samples_per_shot, dimension)
365449
)
366-
# Convert gradients from mT/m to T/m
367-
gradients = np.reshape(gradients * 1e-3, (-1, num_samples_per_shot, dimension))
368-
kspace_loc = np.zeros((num_shots, num_adc_samples, dimension))
450+
if start_skip_samples > 0:
451+
start_location_updates = (
452+
np.sum(gradients[:, :start_skip_samples], axis=1) * raster_time * gamma
453+
)
454+
initial_positions += start_location_updates
455+
gradients = gradients[:, start_skip_samples:, :]
456+
if end_skip_samples > 0:
457+
gradients = gradients[:, :-end_skip_samples, :]
458+
num_samples_per_shot -= start_skip_samples + end_skip_samples
459+
if num_adc_samples is None:
460+
if read_shots:
461+
num_adc_samples = num_samples_per_shot + 1
462+
else:
463+
num_adc_samples = int(num_samples_per_shot * (raster_time / dwell_time))
464+
kspace_loc = np.zeros((num_shots, num_adc_samples, dimension), dtype=np.float32)
369465
kspace_loc[:, 0, :] = initial_positions
370466
adc_times = dwell_time_ns * np.arange(1, num_adc_samples)
371467
Q, R = divmod(adc_times, gradient_raster_time_ns)

0 commit comments

Comments
 (0)