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
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,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
279361def 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