Skip to content

Commit c7fb11b

Browse files
chaithyagrchaithyagrpaquiteauAsma TANABENEDaval-G
authored
Add prephasors, rewinders etc. (#276)
* Added * Fix * Remove bymistake add * Fix * Fixed lint * Lint * Added refbackend * Fix NDFT * feat: use finufft as ref backend. * feat(tests): move ndft vs nufft tests to own file. * Add support for pipe * \!docs_build try to run cufinufft tests * \!docs_build fix style * Added next235 for stability * Fix lint * Fix CUPY * WIP * Updates * fix back learn examples * move tto flatiron * fix black * Move to test on GPU * Update pyproject toml and use it in test-ci, to prevent duplication of dependencies and actually test them! * Make CI build shorter * Test run to run * \!docs_build Added * adding density normalization * Start support for finufft spread and interpolate (pipe) * Add support for density comp * Add support for normlize back * Added a bunch of extra codes * PEP fixes * Update siemens.py * Added fixes * add [docs] * Fixes and updates on the locatuions * Update the codes to be in sync with cufinufft / finufft 2.4.0 * Merged to master * [style] * Fix toml ffile [docs] * Fix toml ffile [docs] * Update testbatch stuff * Update the tests * Fixed tests againnnn * All set * Add finufft * Updates * Fixes * More fixes * make tests less strict again * remove bymistake * Remove strictness, everything works, hopefully [docs] * [docs] setup cufinufft also * [docs] fix style * [docs] more updates, fixes * pywavelets * [docs] fixes further * [docs] try again * [docs] final comments * [docs] method class use * WIP, updated codes to get itw orking * trying to vectorize * WIP * WIP * added direct change * More updates * Added timing and gradients * Update gradspec and codes * Update the error for slew * Added tests * Setup PEP, ready for review * Minor update on spec * WIP * Fixes for tests * fix docs * Update src/mrinufft/io/nsp.py Co-authored-by: Pierre-Antoine Comby <[email protected]> * remove unused stuff * Final fixes. [docs] * Haandleing comments * Update src/mrinufft/trajectories/tools.py Co-authored-by: Guillaume Daval-Frérot <[email protected]> * [docs] update the docnames * update TE_pos * style fixes * style fixes * Fix for ramps * Update src/mrinufft/trajectories/tools.py Co-authored-by: Guillaume Daval-Frérot <[email protected]> * Apply suggestions from code review Co-authored-by: Guillaume Daval-Frérot <[email protected]> * Update src/mrinufft/io/nsp.py Co-authored-by: Guillaume Daval-Frérot <[email protected]> * Update src/mrinufft/io/nsp.py Co-authored-by: Guillaume Daval-Frérot <[email protected]> * Update src/mrinufft/io/nsp.py Co-authored-by: Guillaume Daval-Frérot <[email protected]> * Update src/mrinufft/io/nsp.py Co-authored-by: Guillaume Daval-Frérot <[email protected]> * Handle comments * rename parameters and fix ordering * Handle comments * Update codes * More updates * Fixed some bugs * Update the tools * style fixes and [docs] * More bugs! Fixes * [WIP] * Updateds WIP * wip * WIP: moved to solvers * Fixed everything, ready for review * Update src/mrinufft/trajectories/tools.py Co-authored-by: Pierre-Antoine Comby <[email protected]> * style fixes * Update ruff * Added joblib * style changes [docs] * Apply suggestions from code review Co-authored-by: Pierre-Antoine Comby <[email protected]> * Renaming * Fix more comments * Undo changes * fix undo * refactor: split test in two --------- Co-authored-by: chaithyagr <[email protected]> Co-authored-by: Pierre-antoine Comby <[email protected]> Co-authored-by: Asma TANABENE <[email protected]> Co-authored-by: Guillaume Daval-Frérot <[email protected]> Co-authored-by: Pierre-Antoine Comby <[email protected]>
1 parent 4292bb5 commit c7fb11b

File tree

7 files changed

+560
-41
lines changed

7 files changed

+560
-41
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
+----------------+-------+---------+---------+------------------------------------------------------------------------+

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description = "MRI Non-Cartesian Fourier Operators with multiple computation bac
44
authors = [{name="Pierre-antoine Comby", email="[email protected]"}]
55

66
readme = "README.md"
7-
dependencies = ["numpy", "scipy", "matplotlib", "tqdm"]
7+
dependencies = ["numpy", "scipy", "matplotlib", "tqdm", "joblib"]
88
requires-python = ">=3.9"
99

1010
dynamic = ["version"]

src/mrinufft/io/nsp.py

Lines changed: 124 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from datetime import datetime
99

1010
import numpy as np
11+
from typing import Optional
1112

1213
from mrinufft.trajectories.utils import (
1314
DEFAULT_GMAX,
@@ -19,6 +20,7 @@
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

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

279363
def 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

Comments
 (0)