pulserver 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pulserver/__init__.py +17 -0
- pulserver/_apps/SPGR2D.py +206 -0
- pulserver/_apps/SPGR3D.py +184 -0
- pulserver/_apps/bar.py +15 -0
- pulserver/_apps/foo.py +15 -0
- pulserver/_cli.py +70 -0
- pulserver/_core/__init__.py +12 -0
- pulserver/_core/_autoseg.py +128 -0
- pulserver/_core/_ceq.py +541 -0
- pulserver/_core/_sequence.py +313 -0
- pulserver/_server/__init__.py +6 -0
- pulserver/_server/_server.py +197 -0
- pulserver/_version.py +16 -0
- pulserver/blocks/__init__.py +16 -0
- pulserver/blocks/_grad/_make_spiral.py +201 -0
- pulserver/blocks/_misc/__init__.py +7 -0
- pulserver/blocks/_misc/_make_spoiler_gradient.py +64 -0
- pulserver/blocks/_phaseenc/__init__.py +7 -0
- pulserver/blocks/_phaseenc/_make_phase_blip.py +0 -0
- pulserver/blocks/_phaseenc/_make_phase_encoding.py +49 -0
- pulserver/blocks/_readout/__init__.py +9 -0
- pulserver/blocks/_readout/_make_line_readout.py +149 -0
- pulserver/blocks/_readout/_make_spiral_readout.py +105 -0
- pulserver/blocks/_readout/rosette_readout.py +6 -0
- pulserver/blocks/_rfpulse/__init__.py +221 -0
- pulserver/blocks/_rfpulse/_make_spsp_pulse.py +196 -0
- pulserver/blocks/_rfpulse/_plot.py +164 -0
- pulserver/blocks/_rfpulse/_spsp/__init__.py +11 -0
- pulserver/blocks/_rfpulse/_spsp/_slr.py +444 -0
- pulserver/blocks/_rfpulse/_spsp/_utils.py +99 -0
- pulserver/blocks/_rfpulse/_spsp/rf_ripple.py +75 -0
- pulserver/blocks/_rfpulse/_spsp/spec_interp.py +35 -0
- pulserver/blocks/_rfpulse/_spsp/ss_alias.py +316 -0
- pulserver/blocks/_rfpulse/_spsp/ss_design.py +493 -0
- pulserver/blocks/_rfpulse/_spsp/ss_ep.py +332 -0
- pulserver/blocks/_rfpulse/_spsp/ss_filters.py +2079 -0
- pulserver/blocks/_rfpulse/_spsp/ss_flyback.py +441 -0
- pulserver/blocks/_rfpulse/_spsp/ss_fourier.py +90 -0
- pulserver/blocks/_rfpulse/_spsp/ss_globals.py +43 -0
- pulserver/blocks/_rfpulse/_spsp/ss_grad.py +239 -0
- pulserver/blocks/_rfpulse/_spsp/ss_spect_correct.py +57 -0
- pulserver/blocks/_rfpulse/_spsp/ss_verse.py +166 -0
- pulserver/plan/__init__.py +32 -0
- pulserver/plan/_cartesian2D.py +151 -0
- pulserver/plan/_cartesian3D.py +157 -0
- pulserver/plan/_iterators/__init__.py +19 -0
- pulserver/plan/_iterators/_base.py +69 -0
- pulserver/plan/_iterators/_cartesian2D_iterator.py +173 -0
- pulserver/plan/_iterators/_cartesian3D_iterator.py +137 -0
- pulserver/plan/_iterators/_noncartesian2D_iterator.py +174 -0
- pulserver/plan/_iterators/_noncartesian3D_iterator.py +119 -0
- pulserver/plan/_iterators/_stack3D_iterator.py +173 -0
- pulserver/plan/_noncartesian2D.py +139 -0
- pulserver/plan/_noncartesian3D.py +94 -0
- pulserver/plan/_ordering.py +55 -0
- pulserver/plan/_phase_cycling.py +87 -0
- pulserver/plan/_sampling/__init__.py +14 -0
- pulserver/plan/_sampling/_grid_sampling.py +180 -0
- pulserver/plan/_sampling/_partial_fourier.py +52 -0
- pulserver/plan/_sampling/_poisson_sampling.py +275 -0
- pulserver/plan/_sampling/_tilt.py +48 -0
- pulserver/plan/_stack3D.py +136 -0
- pulserver/sequences/__init__.py +5 -0
- pulserver/sequences/_design_2D_spgr.py +211 -0
- pulserver/sequences/_design_3D_spgr.py +184 -0
- pulserver-0.1.0.dist-info/LICENSE.txt +21 -0
- pulserver-0.1.0.dist-info/METADATA +95 -0
- pulserver-0.1.0.dist-info/RECORD +70 -0
- pulserver-0.1.0.dist-info/WHEEL +5 -0
- pulserver-0.1.0.dist-info/top_level.txt +1 -0
pulserver/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Pulseforge public API."""
|
|
2
|
+
|
|
3
|
+
__all__ = []
|
|
4
|
+
|
|
5
|
+
from pypulseq import Opts # noqa
|
|
6
|
+
|
|
7
|
+
from . import blocks # noqa
|
|
8
|
+
from . import plan # noqa
|
|
9
|
+
from . import sequences # noqa
|
|
10
|
+
|
|
11
|
+
from . import _server # noqa
|
|
12
|
+
|
|
13
|
+
from ._core import Sequence # noqa
|
|
14
|
+
from ._core import SequenceParams # noqa
|
|
15
|
+
|
|
16
|
+
__all__.append("Opts")
|
|
17
|
+
__all__.extend(["Sequence", "SequenceParams"])
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""2D Spoiled Gradient Echo sequence."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["SPGR2D"]
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pypulseq as pp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from pulserver import Sequence
|
|
13
|
+
from pulserver.plan import RfPhaseCycle
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def SPGR2D(
|
|
17
|
+
fov: Iterable[float],
|
|
18
|
+
slice_thickness: float,
|
|
19
|
+
npix: Iterable[int],
|
|
20
|
+
nslices: int,
|
|
21
|
+
alpha: float,
|
|
22
|
+
rectime: float,
|
|
23
|
+
max_grad: float,
|
|
24
|
+
max_slew: float,
|
|
25
|
+
raster_time: float,
|
|
26
|
+
slice_spacing: float = 0.0,
|
|
27
|
+
seqformat: str | bool = "bytes",
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Generate a 2D Spoiled Gradient Recalled Echo (SPGR) pulse sequence.
|
|
31
|
+
|
|
32
|
+
This function designs a 2D SPGR sequence based on the provided field of view (FOV), matrix size,
|
|
33
|
+
number of slices, slice thickness and spacing, flip angle, recovery time,
|
|
34
|
+
and hardware constraints such as maximum gradient amplitude and slew rate.
|
|
35
|
+
The output can be formatted in different sequence file formats if specified.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
fov : Iterable[float]
|
|
40
|
+
Field of view along each spatial dimension [fov_x, fov_y] in mm.
|
|
41
|
+
If scalar, assume squared fov.
|
|
42
|
+
slice_thickness : float)
|
|
43
|
+
Slice thickness in mm.
|
|
44
|
+
npix : Iterable[int]
|
|
45
|
+
Number of voxels along each spatial dimension [nx, ny] (matrix size).
|
|
46
|
+
If scalar, assume squared matrix size.
|
|
47
|
+
nslices : int
|
|
48
|
+
Number of slices.
|
|
49
|
+
alpha : float
|
|
50
|
+
Flip angle in degrees.
|
|
51
|
+
rectime : float
|
|
52
|
+
Recovery time after spoiling in ms.
|
|
53
|
+
max_grad : float
|
|
54
|
+
Maximum gradient amplitude in mT/m.
|
|
55
|
+
max_slew : float
|
|
56
|
+
Maximum gradient slew rate in T/m/s.
|
|
57
|
+
raster_time : float
|
|
58
|
+
Waveform raster time in seconds (the time between successive gradient samples).
|
|
59
|
+
slice_spacing : float, optional
|
|
60
|
+
Additional slice spacing in mm. The default is 0.0 (contiguous slices).
|
|
61
|
+
seqformat : str or bool, optional
|
|
62
|
+
Output sequence format. If a string is provided, it specifies the desired output format (e.g., 'pulseq', 'bytes').
|
|
63
|
+
If False, the sequence is returned as an internal object. Default is False.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
seq : object or dict
|
|
68
|
+
The generated SPGR sequence. If `seqformat` is a string, the sequence is returned in the specified format.
|
|
69
|
+
If `seqformat` is False, the sequence is returned as an internal representation.
|
|
70
|
+
|
|
71
|
+
Notes
|
|
72
|
+
-----
|
|
73
|
+
- This function is designed to work within the constraints of MRI scanners, taking into account the physical limits
|
|
74
|
+
on gradient amplitude and slew rates.
|
|
75
|
+
- The flip angle (`alpha`) controls the excitation of spins and directly impacts the signal-to-noise ratio (SNR) and contrast.
|
|
76
|
+
|
|
77
|
+
Examples
|
|
78
|
+
--------
|
|
79
|
+
Generate a 2D SPGR sequence for a single 5 mm thick slice and 240x240 mm FOV, 256x256 matrix size,
|
|
80
|
+
15-degree flip angle, 5s recovery time and hardware limits 4mT/m, 150T/m/s, 4e-6 s raster time as:
|
|
81
|
+
|
|
82
|
+
>>> from pulseforge import SPGR2D
|
|
83
|
+
>>> SPGR2D(240.0, 5.0, 256, 1, 15.0, 5.0, 40, 150, 4e-6)
|
|
84
|
+
|
|
85
|
+
Generate the same sequence and export it in bytes format:
|
|
86
|
+
|
|
87
|
+
>>> SPGR2D(240.0, 5.0, 256, 1, 15.0, 5.0, 40, 150, 4e-6, format='bytes')
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
# RF specs
|
|
91
|
+
rf_spoiling_inc = 117.0 # RF spoiling increment
|
|
92
|
+
|
|
93
|
+
# initialize system limits
|
|
94
|
+
system = pp.Opts(
|
|
95
|
+
max_grad=max_grad,
|
|
96
|
+
grad_unit="mT/m",
|
|
97
|
+
max_slew=max_slew,
|
|
98
|
+
slew_unit="T/m/s",
|
|
99
|
+
grad_raster_time=raster_time,
|
|
100
|
+
rf_raster_time=raster_time,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# initialize sequence
|
|
104
|
+
seq = Sequence(system=system, format=seqformat)
|
|
105
|
+
|
|
106
|
+
# initialize prescription
|
|
107
|
+
fov, slice_thickness, slice_spacing = (
|
|
108
|
+
fov * 1e-3,
|
|
109
|
+
slice_thickness * 1e-3,
|
|
110
|
+
slice_spacing * 1e-3,
|
|
111
|
+
)
|
|
112
|
+
slice_spacing += slice_thickness
|
|
113
|
+
|
|
114
|
+
if np.isscalar(npix):
|
|
115
|
+
Nx, Ny, Nz = npix, npix, nslices # in-plane resolution, slice thickness
|
|
116
|
+
else:
|
|
117
|
+
Nx, Ny, Nz = npix[0], npix[1], nslices # in-plane resolution, slice thickness
|
|
118
|
+
|
|
119
|
+
# initialize event events
|
|
120
|
+
# RF pulse
|
|
121
|
+
rf, gss, _ = pp.make_sinc_pulse(
|
|
122
|
+
flip_angle=np.deg2rad(alpha),
|
|
123
|
+
duration=3e-3,
|
|
124
|
+
slice_thickness=slice_thickness,
|
|
125
|
+
apodization=0.42,
|
|
126
|
+
time_bw_product=4,
|
|
127
|
+
system=system,
|
|
128
|
+
return_gz=True,
|
|
129
|
+
)
|
|
130
|
+
gss_reph = pp.make_trapezoid(
|
|
131
|
+
channel="z", area=-gss.area / 2, duration=1e-3, system=system
|
|
132
|
+
)
|
|
133
|
+
seq.register_block(name="excitation", rf=rf, gz=gss)
|
|
134
|
+
seq.register_block(name="slab_rephasing", gz=gss_reph)
|
|
135
|
+
|
|
136
|
+
# readout
|
|
137
|
+
delta_kx, delta_ky = 1 / fov, 1 / fov
|
|
138
|
+
g_read = pp.make_trapezoid(
|
|
139
|
+
channel="x", flat_area=Nx * delta_kx, flat_time=3.2e-3, system=system
|
|
140
|
+
)
|
|
141
|
+
adc = pp.make_adc(
|
|
142
|
+
num_samples=Nx, duration=g_read.flat_time, delay=g_read.rise_time, system=system
|
|
143
|
+
)
|
|
144
|
+
seq.register_block("dummy_readout", gx=g_read)
|
|
145
|
+
seq.register_block("readout", gx=g_read, adc=adc)
|
|
146
|
+
|
|
147
|
+
# phase encoding
|
|
148
|
+
gx_phase = pp.make_trapezoid(
|
|
149
|
+
channel="x", area=-g_read.area / 2, duration=1e-3, system=system
|
|
150
|
+
)
|
|
151
|
+
gy_phase = pp.make_trapezoid(channel="y", area=delta_ky * Ny, system=system)
|
|
152
|
+
seq.register_block("g_phase", gx=gx_phase, gy=gy_phase)
|
|
153
|
+
|
|
154
|
+
# crusher gradient
|
|
155
|
+
gz_spoil = pp.make_trapezoid(channel="z", area=32 / slice_thickness, system=system)
|
|
156
|
+
delay = pp.make_delay(rectime)
|
|
157
|
+
seq.register_block("g_spoil", gz=gz_spoil, delay=delay)
|
|
158
|
+
|
|
159
|
+
# phase encoding plan TODO: helper routine
|
|
160
|
+
pey_steps = ((np.arange(Ny)) - (Ny / 2)) / Ny
|
|
161
|
+
FOVz = Nz * slice_spacing
|
|
162
|
+
foff_steps = np.linspace(-FOVz / 2, FOVz / 2, Nz) * gss.amplitude
|
|
163
|
+
encoding_plan = np.meshgrid(pey_steps, foff_steps, indexing="xy")
|
|
164
|
+
encoding_plan = [enc.ravel() for enc in encoding_plan]
|
|
165
|
+
|
|
166
|
+
# scan duration
|
|
167
|
+
dummy_scans = 10
|
|
168
|
+
calib_scans = 10
|
|
169
|
+
imaging_scans = Ny * nslices
|
|
170
|
+
|
|
171
|
+
# generate rf phases
|
|
172
|
+
rf_phases = RfPhaseCycle(
|
|
173
|
+
num_pulses=dummy_scans + imaging_scans + calib_scans,
|
|
174
|
+
phase_increment=rf_spoiling_inc,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# construct sequence
|
|
178
|
+
seq.section(name="ss_prep")
|
|
179
|
+
for n in range(dummy_scans):
|
|
180
|
+
rf_phase = rf_phases()
|
|
181
|
+
seq.add_block("excitation")
|
|
182
|
+
seq.add_block("slab_rephasing")
|
|
183
|
+
seq.add_block("g_phase", gy_amp=0.0)
|
|
184
|
+
seq.add_block("dummy_readout", rf_phase=rf_phase)
|
|
185
|
+
seq.add_block("g_phase", gy_amp=0.0)
|
|
186
|
+
seq.add_block("g_spoil")
|
|
187
|
+
|
|
188
|
+
seq.section(name="scan_loop")
|
|
189
|
+
for n in range(imaging_scans + calib_scans):
|
|
190
|
+
rf_phase = rf_phases()
|
|
191
|
+
|
|
192
|
+
if n < calib_scans:
|
|
193
|
+
rf_freq = 0.0
|
|
194
|
+
y_amp = 0.0
|
|
195
|
+
else:
|
|
196
|
+
rf_freq = encoding_plan[1][n - calib_scans]
|
|
197
|
+
y_amp = encoding_plan[0][n - calib_scans]
|
|
198
|
+
|
|
199
|
+
seq.add_block("excitation", rf_freq=rf_freq)
|
|
200
|
+
seq.add_block("slab_rephasing")
|
|
201
|
+
seq.add_block("g_phase", gy_amp=y_amp)
|
|
202
|
+
seq.add_block("readout", rf_phase=rf_phase, adc_phase=rf_phase)
|
|
203
|
+
seq.add_block("g_phase", gy_amp=-y_amp)
|
|
204
|
+
seq.add_block("g_spoil")
|
|
205
|
+
|
|
206
|
+
return seq.export()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""3D Spoiled Gradient Echo sequence."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["SPGR3D"]
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pypulseq as pp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from pulserver import Sequence
|
|
13
|
+
from pulserver.plan import RfPhaseCycle
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def SPGR3D(
|
|
17
|
+
fov: Iterable[float],
|
|
18
|
+
npix: Iterable[int],
|
|
19
|
+
alpha: float,
|
|
20
|
+
max_grad: float,
|
|
21
|
+
max_slew: float,
|
|
22
|
+
raster_time: float,
|
|
23
|
+
seqformat: str | bool = "bytes",
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Generate a 3D Spoiled Gradient Recalled Echo (SPGR) pulse sequence.
|
|
27
|
+
|
|
28
|
+
This function designs a 3D SPGR sequence based on the provided field of view (FOV), matrix size,
|
|
29
|
+
flip angle, and hardware constraints such as maximum gradient amplitude and slew rate. The output
|
|
30
|
+
can be formatted in different sequence file formats if specified.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
fov : Iterable[float]
|
|
35
|
+
Field of view along each spatial dimension [fov_plane, fov_z] in mm.
|
|
36
|
+
If scalar, assume cubic fov.
|
|
37
|
+
npix : Iterable[int]
|
|
38
|
+
Number of voxels along each spatial dimension [plane_mtx, nz] (matrix size).
|
|
39
|
+
If scalar, assume cubic matrix size.
|
|
40
|
+
alpha : float
|
|
41
|
+
Flip angle in degrees.
|
|
42
|
+
max_grad : float
|
|
43
|
+
Maximum gradient amplitude in mT/m.
|
|
44
|
+
max_slew : float
|
|
45
|
+
Maximum gradient slew rate in T/m/s.
|
|
46
|
+
raster_time : float
|
|
47
|
+
Waveform raster time in seconds (the time between successive gradient samples).
|
|
48
|
+
seqformat : str or bool, optional
|
|
49
|
+
Output sequence format. If a string is provided, it specifies the desired output format (e.g., 'pulseq', 'bytes').
|
|
50
|
+
If False, the sequence is returned as an internal object. Default is False.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
seq : object or dict
|
|
55
|
+
The generated SPGR sequence. If `seqformat` is a string, the sequence is returned in the specified format.
|
|
56
|
+
If `seqformat` is False, the sequence is returned as an internal representation.
|
|
57
|
+
|
|
58
|
+
Notes
|
|
59
|
+
-----
|
|
60
|
+
- This function is designed to work within the constraints of MRI scanners, taking into account the physical limits
|
|
61
|
+
on gradient amplitude and slew rates.
|
|
62
|
+
- The flip angle (`alpha`) controls the excitation of spins and directly impacts the signal-to-noise ratio (SNR) and contrast.
|
|
63
|
+
|
|
64
|
+
Examples
|
|
65
|
+
--------
|
|
66
|
+
Generate a 3D SPGR sequence for a 256x256x128 matrix with a 240x240x120 mm FOV, a 15-degree flip angle, and hardware limits:
|
|
67
|
+
|
|
68
|
+
>>> from pulseforge import SPGR3D
|
|
69
|
+
>>> SPGR3D([240, 120], [256, 128], 15, 0.04, 150, 4e-6)
|
|
70
|
+
|
|
71
|
+
Generate the same sequence and export it in bytes format:
|
|
72
|
+
|
|
73
|
+
>>> SPGR3D([240, 120], [256, 128], 15, 0.04, 150, 4e-6, seqformat='bytes')
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
# RF specs
|
|
77
|
+
rf_spoiling_inc = 117.0 # RF spoiling increment
|
|
78
|
+
|
|
79
|
+
# initialize system limits
|
|
80
|
+
system = pp.Opts(
|
|
81
|
+
max_grad=max_grad,
|
|
82
|
+
grad_unit="mT/m",
|
|
83
|
+
max_slew=max_slew,
|
|
84
|
+
slew_unit="T/m/s",
|
|
85
|
+
grad_raster_time=raster_time,
|
|
86
|
+
rf_raster_time=raster_time,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# initialize sequence
|
|
90
|
+
seq = Sequence(system=system, format=seqformat)
|
|
91
|
+
|
|
92
|
+
# initialize prescription
|
|
93
|
+
if np.isscalar(fov):
|
|
94
|
+
fov, slab_thickness = fov * 1e-3, fov * 1e-3 # isotropic
|
|
95
|
+
else:
|
|
96
|
+
fov, slab_thickness = (
|
|
97
|
+
fov[0] * 1e-3,
|
|
98
|
+
fov[1] * 1e-3,
|
|
99
|
+
) # in-plane FOV, slab thickness
|
|
100
|
+
|
|
101
|
+
if np.isscalar(npix):
|
|
102
|
+
Nx, Ny, Nz = npix, npix, npix # in-plane resolution, slice thickness
|
|
103
|
+
else:
|
|
104
|
+
Nx, Ny, Nz = npix[0], npix[0], npix[1] # in-plane resolution, slice thickness
|
|
105
|
+
|
|
106
|
+
# initialize event events
|
|
107
|
+
# RF pulse
|
|
108
|
+
rf, gss, _ = pp.make_sinc_pulse(
|
|
109
|
+
flip_angle=np.deg2rad(alpha),
|
|
110
|
+
duration=3e-3,
|
|
111
|
+
slice_thickness=slab_thickness,
|
|
112
|
+
apodization=0.42,
|
|
113
|
+
time_bw_product=4,
|
|
114
|
+
system=system,
|
|
115
|
+
return_gz=True,
|
|
116
|
+
)
|
|
117
|
+
gss_reph = pp.make_trapezoid(
|
|
118
|
+
channel="z", area=-gss.area / 2, duration=1e-3, system=system
|
|
119
|
+
)
|
|
120
|
+
seq.register_block(name="excitation", rf=rf, gz=gss)
|
|
121
|
+
seq.register_block(name="slab_rephasing", gz=gss_reph)
|
|
122
|
+
|
|
123
|
+
# readout
|
|
124
|
+
delta_kx, delta_ky, delta_kz = 1 / fov, 1 / fov, 1 / slab_thickness
|
|
125
|
+
g_read = pp.make_trapezoid(
|
|
126
|
+
channel="x", flat_area=Nx * delta_kx, flat_time=3.2e-3, system=system
|
|
127
|
+
)
|
|
128
|
+
adc = pp.make_adc(
|
|
129
|
+
num_samples=Nx, duration=g_read.flat_time, delay=g_read.rise_time, system=system
|
|
130
|
+
)
|
|
131
|
+
seq.register_block("dummy_readout", gx=g_read)
|
|
132
|
+
seq.register_block("readout", gx=g_read, adc=adc)
|
|
133
|
+
|
|
134
|
+
# phase encoding
|
|
135
|
+
gx_phase = pp.make_trapezoid(
|
|
136
|
+
channel="x", area=-g_read.area / 2, duration=1e-3, system=system
|
|
137
|
+
)
|
|
138
|
+
gy_phase = pp.make_trapezoid(channel="y", area=delta_ky * Ny, system=system)
|
|
139
|
+
gz_phase = pp.make_trapezoid(channel="z", area=delta_kz * Nz, system=system)
|
|
140
|
+
seq.register_block("g_phase", gx=gx_phase, gy=gy_phase, gz=gz_phase)
|
|
141
|
+
|
|
142
|
+
# crusher gradient
|
|
143
|
+
gz_spoil = pp.make_trapezoid(channel="z", area=32 / slab_thickness, system=system)
|
|
144
|
+
seq.register_block("g_spoil", gz=gz_spoil)
|
|
145
|
+
|
|
146
|
+
# phase encoding plan TODO: helper routine
|
|
147
|
+
pey_steps = ((np.arange(Ny)) - (Ny / 2)) / Ny
|
|
148
|
+
pez_steps = ((np.arange(Nz)) - (Nz / 2)) / Nz
|
|
149
|
+
encoding_plan = np.meshgrid(pey_steps, pez_steps, indexing="xy")
|
|
150
|
+
encoding_plan = [enc.ravel() for enc in encoding_plan]
|
|
151
|
+
|
|
152
|
+
# scan duration
|
|
153
|
+
dummy_scans = Ny
|
|
154
|
+
imaging_scans = Ny * Nz
|
|
155
|
+
|
|
156
|
+
# generate rf phases
|
|
157
|
+
rf_phases = RfPhaseCycle(
|
|
158
|
+
num_pulses=dummy_scans + imaging_scans, phase_increment=rf_spoiling_inc
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# construct sequence
|
|
162
|
+
seq.section(name="ss_prep")
|
|
163
|
+
for n in range(dummy_scans):
|
|
164
|
+
rf_phase = rf_phases()
|
|
165
|
+
seq.add_block("excitation")
|
|
166
|
+
seq.add_block("slab_rephasing")
|
|
167
|
+
seq.add_block("g_phase", gy_amp=0.0, gz_amp=0.0)
|
|
168
|
+
seq.add_block("dummy_readout", rf_phase=rf_phase)
|
|
169
|
+
seq.add_block("g_phase", gy_amp=0.0, gz_amp=0.0)
|
|
170
|
+
seq.add_block("g_spoil")
|
|
171
|
+
|
|
172
|
+
seq.section(name="scan_loop")
|
|
173
|
+
for n in range(imaging_scans):
|
|
174
|
+
rf_phase = rf_phases()
|
|
175
|
+
seq.add_block("excitation")
|
|
176
|
+
seq.add_block("slab_rephasing")
|
|
177
|
+
seq.add_block("g_phase", gy_amp=encoding_plan[0][n], gz_amp=encoding_plan[1][n])
|
|
178
|
+
seq.add_block("readout", rf_phase=rf_phase, adc_phase=rf_phase)
|
|
179
|
+
seq.add_block(
|
|
180
|
+
"g_phase", gy_amp=-encoding_plan[0][n], gz_amp=-encoding_plan[1][n]
|
|
181
|
+
)
|
|
182
|
+
seq.add_block("g_spoil")
|
|
183
|
+
|
|
184
|
+
return seq.export()
|
pulserver/_apps/bar.py
ADDED
pulserver/_apps/foo.py
ADDED
pulserver/_cli.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Command Line Interface for server."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from . import _server
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def cli():
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command()
|
|
16
|
+
@click.option("--config-path", default=None, help="Location of config.yml file.")
|
|
17
|
+
@click.option("--scanner-address", default=None, help="IP address of MR scanner.")
|
|
18
|
+
@click.option("--scanner-port", default=None, help="MR scanner port.")
|
|
19
|
+
@click.option(
|
|
20
|
+
"--recon-server-address", default=None, help="IP address of reconstruction servr."
|
|
21
|
+
)
|
|
22
|
+
@click.option("--recon-server-port", default=None, help="Reconstruction server port.")
|
|
23
|
+
def start(
|
|
24
|
+
config_path, scanner_address, scanner_port, recon_server_address, recon_server_port
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Start sequence design server.
|
|
28
|
+
"""
|
|
29
|
+
if config_path is not None:
|
|
30
|
+
os.environ["PULSEFORGE_CONFIG"] = config_path
|
|
31
|
+
else:
|
|
32
|
+
if scanner_address is not None:
|
|
33
|
+
os.environ["PULSEFORGE_SCANNER_ADDRESS"] = scanner_address
|
|
34
|
+
if scanner_port is not None:
|
|
35
|
+
os.environ["PULSEFORGE_SCANNER_PORT"] = scanner_port
|
|
36
|
+
if recon_server_address is not None:
|
|
37
|
+
os.environ["PULSEFORGE_RECON_SERVER_ADDRESS"] = recon_server_address
|
|
38
|
+
if recon_server_port is not None:
|
|
39
|
+
os.environ["PULSEFORGE_RECON_SERVER_PORT"] = recon_server_port
|
|
40
|
+
|
|
41
|
+
# start the server
|
|
42
|
+
_server.start_server()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.command()
|
|
46
|
+
@click.option(
|
|
47
|
+
"--name",
|
|
48
|
+
default=None,
|
|
49
|
+
help="Function whose docstring we want to see. If not provided, print list of available commands.",
|
|
50
|
+
)
|
|
51
|
+
def apps(name):
|
|
52
|
+
"""
|
|
53
|
+
Design function documentations
|
|
54
|
+
"""
|
|
55
|
+
# Load plugin list
|
|
56
|
+
plugins = _server.load_plugins()
|
|
57
|
+
if name is None:
|
|
58
|
+
for fun in plugins.values():
|
|
59
|
+
name = fun.__name__
|
|
60
|
+
summary = fun.__doc__.split("\n")[1]
|
|
61
|
+
click.echo(name + "\t" + summary)
|
|
62
|
+
else:
|
|
63
|
+
click.echo(plugins[name].__doc__)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
cli.add_command(start)
|
|
67
|
+
cli.add_command(apps)
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
cli()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Core Sequence representations.
|
|
2
|
+
|
|
3
|
+
This is an intermediate representation to handle both
|
|
4
|
+
native pypulseq Sequence and Ceq structure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__all__ = []
|
|
8
|
+
|
|
9
|
+
from ._ceq import SequenceParams # noqa
|
|
10
|
+
from ._sequence import Sequence # noqa
|
|
11
|
+
|
|
12
|
+
__all__.extend(["Sequence", "SequenceParams"])
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Automatic segment definition subroutines."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["find_segment_definitions", "find_segments", "split_rotated_segments"]
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numba as nb
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def find_segment_definitions(arr):
|
|
10
|
+
# here, we make the (reasonable?) assumption
|
|
11
|
+
# that all the "prescan", dummy / syncronization / calibration
|
|
12
|
+
# scans are performed at the beginning of the sequence,
|
|
13
|
+
# then we have a long periodic main loop. We want to find the beginning
|
|
14
|
+
# of the main loop
|
|
15
|
+
main_loop, dummy_loop = _find_periodic_pattern(arr.astype(int))
|
|
16
|
+
|
|
17
|
+
if len(dummy_loop) > 0: # TODO: make this recursive?
|
|
18
|
+
# here, we make the (reasonable?) assumption
|
|
19
|
+
# that all the "prescan", dummy / syncronization / calibration
|
|
20
|
+
# scans are also periodic
|
|
21
|
+
dummy_loop, rem = _find_periodic_pattern(dummy_loop.astype(int))
|
|
22
|
+
|
|
23
|
+
# hopefully rem is empty; if it is not, append at the beginning
|
|
24
|
+
if len(rem) > 0 and len(dummy_loop) == 0:
|
|
25
|
+
dummy_loop = [rem.tolist()]
|
|
26
|
+
elif len(rem) > 0:
|
|
27
|
+
dummy_loop = [rem.tolist() + dummy_loop.tolist()]
|
|
28
|
+
else:
|
|
29
|
+
dummy_loop = [dummy_loop.tolist()]
|
|
30
|
+
if len(main_loop) > 0:
|
|
31
|
+
return dummy_loop + [main_loop.tolist()]
|
|
32
|
+
return dummy_loop
|
|
33
|
+
|
|
34
|
+
return [main_loop.tolist()]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_segments(array, subarray):
|
|
38
|
+
arr = nb.typed.List(array)
|
|
39
|
+
subarr = nb.typed.List(subarray)
|
|
40
|
+
return list(_find_segments(arr, subarr))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@nb.njit(cache=True, fastmath=True)
|
|
44
|
+
def _find_segments(array, subarray):
|
|
45
|
+
n = len(array)
|
|
46
|
+
m = len(subarray)
|
|
47
|
+
result = np.zeros(n, dtype=np.bool_)
|
|
48
|
+
|
|
49
|
+
for i in range(n - m + 1):
|
|
50
|
+
match = True
|
|
51
|
+
for j in range(m):
|
|
52
|
+
if array[i + j] != subarray[j]:
|
|
53
|
+
match = False
|
|
54
|
+
break
|
|
55
|
+
if match:
|
|
56
|
+
for k in range(m):
|
|
57
|
+
result[i + k] = True
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def split_rotated_segments(input):
|
|
63
|
+
output = []
|
|
64
|
+
for segment in input:
|
|
65
|
+
tmp = _split_signed_blocks(segment)
|
|
66
|
+
for item in tmp:
|
|
67
|
+
output.append(item)
|
|
68
|
+
return output
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# %% local utils
|
|
72
|
+
def _principal_period(s):
|
|
73
|
+
i = (s + s).find(s, 1, -1)
|
|
74
|
+
return None if i == -1 else s[:i]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _find_periodic_pattern(arr):
|
|
78
|
+
numel = len(arr)
|
|
79
|
+
loop = _principal_period(arr.tobytes())
|
|
80
|
+
|
|
81
|
+
for start in range(numel):
|
|
82
|
+
loop = _principal_period(arr[start:].tobytes())
|
|
83
|
+
if loop is not None:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
if loop is not None:
|
|
87
|
+
loop = np.frombuffer(loop, dtype=int)
|
|
88
|
+
remainder = np.frombuffer(arr[:start], dtype=int)
|
|
89
|
+
else:
|
|
90
|
+
loop = np.frombuffer(arr, dtype=int)
|
|
91
|
+
remainder = np.asarray([], dtype=int)
|
|
92
|
+
return loop, remainder
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Here we make the (I think) reasonable assumption that rotated blocks with different angles
|
|
96
|
+
# (e.g., two consecutive (spiral arm, spiral rewinder) pairs with different angles)
|
|
97
|
+
# are separated by non rotated blocks (e.g., RF pulses or z-spoilers)
|
|
98
|
+
def _split_signed_blocks(arr):
|
|
99
|
+
if not arr:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
# Convert the input list to a numpy array
|
|
103
|
+
arr = np.array(arr)
|
|
104
|
+
|
|
105
|
+
# Create a sign array where positive numbers are marked as 1, negative as -1
|
|
106
|
+
sign = np.sign(arr)
|
|
107
|
+
sign[sign == 0] = 1
|
|
108
|
+
|
|
109
|
+
# Find the indices where the sign changes
|
|
110
|
+
change_indices = np.where(np.diff(sign) != 0)[0] + 1
|
|
111
|
+
|
|
112
|
+
# Split the array at the change indices
|
|
113
|
+
split_blocks = np.split(arr, change_indices)
|
|
114
|
+
|
|
115
|
+
# Convert the numpy arrays in the list to python lists
|
|
116
|
+
result = [list(abs(block)) for block in split_blocks]
|
|
117
|
+
|
|
118
|
+
# Get sizes of each block
|
|
119
|
+
osize = np.asarray([len(block) for block in result])
|
|
120
|
+
max_size = max(osize)
|
|
121
|
+
tmp = [np.pad(result[n], (0, max_size - osize[n])) for n in range(len(result))]
|
|
122
|
+
tmp = np.stack(tmp, axis=0)
|
|
123
|
+
tmp, idx = np.unique(tmp, return_index=True, axis=0)
|
|
124
|
+
tmp = tmp[np.argsort(idx)]
|
|
125
|
+
osize = osize[np.sort(idx)]
|
|
126
|
+
result = [tmp[n][: osize[n]].tolist() for n in range(len(osize))]
|
|
127
|
+
|
|
128
|
+
return result
|