88from datetime import datetime
99
1010import numpy as np
11+ from typing import Optional
1112
1213from mrinufft .trajectories .utils import (
1314 DEFAULT_GMAX ,
1920 convert_gradients_to_slew_rates ,
2021 convert_trajectory_to_gradients ,
2122)
23+ from mrinufft .trajectories .tools import get_gradient_amplitudes_to_travel_for_set_time
2224
2325from .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,8 +214,11 @@ 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 ,
220+ pregrad : str | None = None ,
221+ postgrad : str | None = None ,
205222 version : float = 5 ,
206223 ** kwargs ,
207224):
@@ -226,10 +243,26 @@ 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 prephasing 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 k-space
260+ along x-axis for all the shots i.e. go to (Kmax, 0, 0).
261+ This is useful for sequences needing a spoiler at the end of the trajectory.
262+ However, spoiler is still not added, it is expected that the sequence
263+ handles the spoilers, which can be variable.
264+ `slowdown_to_center` will add a gradient to slow down to the center
265+ of the k-space.
233266 version: float, optional
234267 Trajectory versioning, by default 5
235268 kwargs : dict, optional
@@ -245,7 +278,46 @@ def write_trajectory(
245278 gamma = gamma ,
246279 get_final_positions = True ,
247280 )
248-
281+ Ns_to_skip_at_start = 0
282+ Ns_to_skip_at_end = 0
283+ scan_consts = {
284+ "gamma" : gamma ,
285+ "gmax" : gmax ,
286+ "smax" : smax ,
287+ "raster_time" : raster_time ,
288+ }
289+ if pregrad == "prephase" :
290+ if version < 5.1 :
291+ raise ValueError (
292+ "pregrad is only supported for version >= 5.1, "
293+ "please set version to 5.1 or higher."
294+ )
295+ start_gradients = get_gradient_amplitudes_to_travel_for_set_time (
296+ kspace_end_loc = initial_positions ,
297+ end_gradients = gradients [:, 0 ],
298+ ** scan_consts ,
299+ )
300+ initial_positions = np .zeros_like (initial_positions )
301+ gradients = np .hstack ([start_gradients , gradients ])
302+ Ns_to_skip_at_start = start_gradients .shape [1 ]
303+ if postgrad :
304+ if version < 5.1 :
305+ raise ValueError (
306+ "postgrad is only supported for version >= 5.1, "
307+ "please set version to 5.1 or higher."
308+ )
309+ edge_locations = np .zeros_like (final_positions )
310+ if postgrad == "slowdown_to_edge" :
311+ # Always end at KMax, the spoilers can be handeled by the sequence.
312+ edge_locations [..., 0 ] = img_size [0 ] / FOV [0 ] / 2
313+ end_gradients = get_gradient_amplitudes_to_travel_for_set_time (
314+ kspace_end_loc = edge_locations ,
315+ start_gradients = gradients [:, - 1 ],
316+ kspace_start_loc = final_positions ,
317+ ** scan_consts ,
318+ )
319+ gradients = np .hstack ([gradients , end_gradients ])
320+ Ns_to_skip_at_end = end_gradients .shape [1 ]
249321 # Check constraints if requested
250322 if check_constraints :
251323 slewrates , _ = convert_gradients_to_slew_rates (gradients , raster_time )
@@ -261,6 +333,15 @@ def write_trajectory(
261333 f"Maximum gradient amplitude: { maxG :.3f} > { gmax :.3f} "
262334 f"Maximum slew rate: { maxS :.3f} > { smax :.3f} "
263335 )
336+ if pregrad != "prephase" :
337+ border_slew_rate = gradients [:, 0 ] / raster_time
338+ if np .any (np .abs (border_slew_rate ) > smax ):
339+ warnings .warn (
340+ "Slew rate at start of trajectory exceeds maximum slew rate!"
341+ f"Maximum slew rate: { np .max (np .abs (border_slew_rate )):.3f} "
342+ f" > { smax :.3f} . Please use prephase gradient to avoid this "
343+ " issue."
344+ )
264345
265346 # Write gradients in file
266347 write_gradients (
@@ -270,16 +351,19 @@ def write_trajectory(
270351 grad_filename = grad_filename ,
271352 img_size = img_size ,
272353 FOV = FOV ,
354+ TE_pos = TE_pos ,
273355 gamma = gamma ,
274356 version = version ,
357+ start_skip_samples = Ns_to_skip_at_start ,
358+ end_skip_samples = Ns_to_skip_at_end ,
275359 ** kwargs ,
276360 )
277361
278362
279363def read_trajectory (
280364 grad_filename : str ,
281365 dwell_time : float | str = DEFAULT_RASTER_TIME ,
282- num_adc_samples : int = None ,
366+ num_adc_samples : int | None = None ,
283367 gamma : Gammas | float = Gammas .HYDROGEN ,
284368 raster_time : float = DEFAULT_RASTER_TIME ,
285369 read_shots : bool = False ,
@@ -336,25 +420,27 @@ def read_trajectory(
336420 if dwell_time == "min_osf" :
337421 dwell_time = raster_time / min_osf
338422 (num_shots , num_samples_per_shot ), data = _pop_elements (data , 2 , type = "int" )
339- if num_adc_samples is None :
340- if read_shots :
341- num_adc_samples = num_samples_per_shot + 1
342- else :
343- num_adc_samples = int (num_samples_per_shot * (raster_time / dwell_time ))
344- if version >= 4.1 :
423+ if version > 4 :
345424 TE , data = _pop_elements (data )
346425 grad_max , data = _pop_elements (data )
347426 recon_tag , data = _pop_elements (data )
348427 recon_tag = np .around (recon_tag , 2 )
349428 left_over = 10
350- if version >= 4.2 :
429+ if version > 4.1 :
351430 timestamp , data = _pop_elements (data )
352431 timestamp = datetime .fromtimestamp (float (timestamp ))
353432 left_over -= 1
433+ if version > 5 :
434+ packed_skips , data = _pop_elements (data , num_elements = 2 , type = "int" )
435+ start_skip_samples , end_skip_samples = packed_skips
436+ left_over -= 2
437+ else :
438+ start_skip_samples = 0
439+ end_skip_samples = 0
354440 _ , data = _pop_elements (data , left_over )
355441 initial_positions , data = _pop_elements (data , dimension * num_shots )
356442 initial_positions = np .reshape (initial_positions , (num_shots , dimension ))
357- if version >= 5 :
443+ if version > 4. 5 :
358444 final_positions , data = _pop_elements (data , dimension * num_shots )
359445 final_positions = np .reshape (final_positions , (num_shots , dimension ))
360446 dwell_time_ns = dwell_time * 1e6
@@ -365,12 +451,28 @@ def read_trajectory(
365451 data ,
366452 dimension * num_samples_per_shot * num_shots ,
367453 )
454+ # Convert gradients to T/m
368455 gradients = np .reshape (
369- grad_max * gradients , (num_shots * num_samples_per_shot , dimension )
456+ grad_max * gradients * 1e-3 , (num_shots , num_samples_per_shot , dimension )
370457 )
371- # Convert gradients from mT/m to T/m
372- gradients = np .reshape (gradients * 1e-3 , (- 1 , num_samples_per_shot , dimension ))
373- kspace_loc = np .zeros ((num_shots , num_adc_samples , dimension ))
458+ # Handle skipped samples
459+ if start_skip_samples > 0 :
460+ start_location_updates = (
461+ np .sum (gradients [:, :start_skip_samples ], axis = 1 ) * raster_time * gamma
462+ )
463+ initial_positions += start_location_updates
464+ gradients = gradients [:, start_skip_samples :, :]
465+ num_samples_per_shot -= start_skip_samples
466+ if end_skip_samples > 0 :
467+ gradients = gradients [:, :- end_skip_samples , :]
468+ num_samples_per_shot -= end_skip_samples
469+ if num_adc_samples is None :
470+ if read_shots :
471+ # Acquire one extra sample at the end of each shot in read_shots mode
472+ num_adc_samples = num_samples_per_shot + 1
473+ else :
474+ num_adc_samples = int (num_samples_per_shot * (raster_time / dwell_time ))
475+ kspace_loc = np .zeros ((num_shots , num_adc_samples , dimension ), dtype = np .float32 )
374476 kspace_loc [:, 0 , :] = initial_positions
375477 adc_times = dwell_time_ns * np .arange (1 , num_adc_samples )
376478 Q , R = divmod (adc_times , gradient_raster_time_ns )
0 commit comments