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/_core/_ceq.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"""Ceq structure definition."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["Ceq", "PulseqBlock", "SequenceParams"]
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import asdict as _asdict
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pypulseq as pp
|
|
13
|
+
|
|
14
|
+
from . import _autoseg
|
|
15
|
+
|
|
16
|
+
CHANNEL_ENUM = {"osc0": 0, "osc1": 1, "ext1": 2}
|
|
17
|
+
SEGMENT_RINGDOWN_TIME = 116 * 1e-6 # TODO: doesn't have to be hardcoded
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PulseqShapeArbitrary:
|
|
22
|
+
n_samples: int
|
|
23
|
+
raster: float
|
|
24
|
+
time: np.ndarray
|
|
25
|
+
magnitude: np.ndarray
|
|
26
|
+
phase: np.ndarray | None = None
|
|
27
|
+
amplitude: float | None = None
|
|
28
|
+
|
|
29
|
+
def __post_init__(self):
|
|
30
|
+
self.magnitude = np.asarray(self.magnitude, dtype=np.float32)
|
|
31
|
+
self.time = (
|
|
32
|
+
np.asarray(self.time, dtype=np.float32) if self.time is not None else None
|
|
33
|
+
)
|
|
34
|
+
self.phase = (
|
|
35
|
+
np.asarray(self.phase, dtype=np.float32) if self.phase is not None else None
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# determine amplitude and normalize waveform
|
|
39
|
+
self.amplitude = abs(self.magnitude).max()
|
|
40
|
+
self.magnitude *= 32767 / self.amplitude
|
|
41
|
+
|
|
42
|
+
def to_bytes(self, endian=">") -> bytes:
|
|
43
|
+
_bytes = struct.pack(endian + "i", self.n_samples) + struct.pack(
|
|
44
|
+
endian + "f", self.raster
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# add time
|
|
48
|
+
if self.time is not None:
|
|
49
|
+
_bytes += self.time.astype(endian + "f4").tobytes()
|
|
50
|
+
|
|
51
|
+
# add magnitude
|
|
52
|
+
_bytes += self.magnitude.astype(endian + "f4").tobytes()
|
|
53
|
+
|
|
54
|
+
# add phase
|
|
55
|
+
if self.phase is not None:
|
|
56
|
+
_bytes += self.phase.astype(endian + "f4").tobytes()
|
|
57
|
+
|
|
58
|
+
# add amplitude
|
|
59
|
+
_bytes += struct.pack(endian + "f", self.amplitude)
|
|
60
|
+
|
|
61
|
+
return _bytes
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class PulseqShapeTrap:
|
|
66
|
+
amplitude: float
|
|
67
|
+
rise_time: float
|
|
68
|
+
flat_time: float
|
|
69
|
+
fall_time: float
|
|
70
|
+
|
|
71
|
+
def to_bytes(self, endian=">") -> bytes:
|
|
72
|
+
return (
|
|
73
|
+
struct.pack(endian + "f", self.amplitude)
|
|
74
|
+
+ struct.pack(endian + "f", self.rise_time)
|
|
75
|
+
+ struct.pack(endian + "f", self.flat_time)
|
|
76
|
+
+ struct.pack(endian + "f", self.fall_time)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class PulseqRF:
|
|
82
|
+
type: int
|
|
83
|
+
wav: PulseqShapeArbitrary
|
|
84
|
+
duration: float
|
|
85
|
+
delay: float
|
|
86
|
+
|
|
87
|
+
def to_bytes(self, endian=">") -> bytes:
|
|
88
|
+
return (
|
|
89
|
+
struct.pack(endian + "h", self.type)
|
|
90
|
+
+ self.wav.to_bytes(endian)
|
|
91
|
+
+ struct.pack(endian + "f", self.duration)
|
|
92
|
+
+ struct.pack(endian + "f", self.delay)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_struct(cls, data: SimpleNamespace) -> "PulseqRF":
|
|
97
|
+
n_samples = data.signal.shape[0]
|
|
98
|
+
|
|
99
|
+
# determine whether wave is arbitrary or extended trap
|
|
100
|
+
dt = np.unique(np.diff(np.round(data.t * 1e6))) / 1e6
|
|
101
|
+
|
|
102
|
+
if len(dt) == 1: # uniform raster -> arbitrary shape
|
|
103
|
+
type = 1
|
|
104
|
+
raster = dt.item()
|
|
105
|
+
time = None
|
|
106
|
+
else: # non-uniform raster -> extended trapezoid
|
|
107
|
+
type = 2
|
|
108
|
+
raster = 0.0
|
|
109
|
+
time = data.t
|
|
110
|
+
|
|
111
|
+
rho = np.abs(data.signal)
|
|
112
|
+
theta = np.angle(data.signal)
|
|
113
|
+
|
|
114
|
+
return cls(
|
|
115
|
+
type=type,
|
|
116
|
+
wav=PulseqShapeArbitrary(n_samples, raster, time, rho, theta),
|
|
117
|
+
duration=data.shape_dur,
|
|
118
|
+
delay=data.delay,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class PulseqGrad:
|
|
124
|
+
type: int
|
|
125
|
+
delay: float
|
|
126
|
+
shape: PulseqShapeArbitrary | PulseqShapeTrap
|
|
127
|
+
|
|
128
|
+
def to_bytes(self, endian=">") -> bytes:
|
|
129
|
+
return (
|
|
130
|
+
struct.pack(endian + "h", self.type)
|
|
131
|
+
+ struct.pack(endian + "f", self.delay)
|
|
132
|
+
+ self.shape.to_bytes(endian)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_struct(cls, data: SimpleNamespace) -> "PulseqGrad":
|
|
137
|
+
print(data)
|
|
138
|
+
if data.type == "trap":
|
|
139
|
+
type = 1
|
|
140
|
+
shape_obj = PulseqShapeTrap(
|
|
141
|
+
data.amplitude, data.rise_time, data.flat_time, data.fall_time
|
|
142
|
+
)
|
|
143
|
+
elif data.type == "grad":
|
|
144
|
+
n_samples = data.waveform.shape[0]
|
|
145
|
+
|
|
146
|
+
# determine whether wave is arbitrary or extended trap
|
|
147
|
+
dt = np.unique(np.diff(np.round(data.tt * 1e6))) / 1e6
|
|
148
|
+
|
|
149
|
+
if len(dt) == 1: # uniform raster -> arbitrary shape
|
|
150
|
+
print("arbitrary")
|
|
151
|
+
type = 2
|
|
152
|
+
raster = dt.item()
|
|
153
|
+
time = None
|
|
154
|
+
else: # non-uniform raster -> extended trapezoid
|
|
155
|
+
print("extended")
|
|
156
|
+
type = 3
|
|
157
|
+
raster = 0.0
|
|
158
|
+
time = data.tt
|
|
159
|
+
|
|
160
|
+
waveform = data.waveform
|
|
161
|
+
shape_obj = PulseqShapeArbitrary(n_samples, raster, time, waveform)
|
|
162
|
+
|
|
163
|
+
return cls(type=type, delay=data.delay, shape=shape_obj)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class PulseqADC:
|
|
168
|
+
type: int
|
|
169
|
+
num_samples: int
|
|
170
|
+
dwell: float
|
|
171
|
+
delay: float
|
|
172
|
+
|
|
173
|
+
def to_bytes(self, endian=">") -> bytes:
|
|
174
|
+
return (
|
|
175
|
+
struct.pack(endian + "h", self.type)
|
|
176
|
+
+ struct.pack(endian + "i", self.num_samples)
|
|
177
|
+
+ struct.pack(endian + "f", self.dwell)
|
|
178
|
+
+ struct.pack(endian + "f", self.delay)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def from_struct(cls, data: SimpleNamespace) -> "PulseqADC":
|
|
183
|
+
return cls(
|
|
184
|
+
type=1,
|
|
185
|
+
num_samples=data.num_samples,
|
|
186
|
+
dwell=data.dwell,
|
|
187
|
+
delay=data.delay,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass
|
|
192
|
+
class PulseqTrig:
|
|
193
|
+
type: int
|
|
194
|
+
channel: int
|
|
195
|
+
delay: float
|
|
196
|
+
duration: float
|
|
197
|
+
|
|
198
|
+
def to_bytes(self, endian=">") -> bytes:
|
|
199
|
+
return (
|
|
200
|
+
struct.pack(endian + "h", self.type)
|
|
201
|
+
+ struct.pack(endian + "i", self.channel)
|
|
202
|
+
+ struct.pack(endian + "f", self.delay)
|
|
203
|
+
+ struct.pack(endian + "f", self.duration)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_struct(cls, data: SimpleNamespace) -> "PulseqTrig":
|
|
208
|
+
return cls(
|
|
209
|
+
type=1,
|
|
210
|
+
channel=CHANNEL_ENUM[data.channel],
|
|
211
|
+
delay=data.delay,
|
|
212
|
+
duration=data.duration,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class PulseqBlock:
|
|
217
|
+
"""Pulseq block structure."""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
ID: int,
|
|
222
|
+
rf: SimpleNamespace = None,
|
|
223
|
+
gx: SimpleNamespace = None,
|
|
224
|
+
gy: SimpleNamespace = None,
|
|
225
|
+
gz: SimpleNamespace = None,
|
|
226
|
+
adc: SimpleNamespace = None,
|
|
227
|
+
trig: SimpleNamespace = None,
|
|
228
|
+
delay: SimpleNamespace = None,
|
|
229
|
+
) -> "PulseqBlock":
|
|
230
|
+
self.ID = ID
|
|
231
|
+
args = [rf, gx, gy, gz, adc, trig, delay]
|
|
232
|
+
args = [arg for arg in args if arg is not None]
|
|
233
|
+
self.duration = pp.calc_duration(*args)
|
|
234
|
+
self.rf = PulseqRF.from_struct(rf) if rf else None
|
|
235
|
+
self.gx = PulseqGrad.from_struct(gx) if gx else None
|
|
236
|
+
self.gy = PulseqGrad.from_struct(gy) if gy else None
|
|
237
|
+
self.gz = PulseqGrad.from_struct(gz) if gz else None
|
|
238
|
+
self.adc = PulseqADC.from_struct(adc) if adc else None
|
|
239
|
+
self.trig = PulseqTrig.from_struct(trig) if trig else None
|
|
240
|
+
|
|
241
|
+
def to_bytes(self, endian=">") -> bytes: # noqa
|
|
242
|
+
bytes_data = struct.pack(endian + "i", self.ID) + struct.pack(
|
|
243
|
+
endian + "f", self.duration
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# RF Event
|
|
247
|
+
if self.rf:
|
|
248
|
+
bytes_data += self.rf.to_bytes(endian)
|
|
249
|
+
else:
|
|
250
|
+
bytes_data += struct.pack(endian + "h", 0)
|
|
251
|
+
|
|
252
|
+
# Gradient Events
|
|
253
|
+
for grad in [self.gx, self.gy, self.gz]:
|
|
254
|
+
if grad:
|
|
255
|
+
bytes_data += grad.to_bytes(endian)
|
|
256
|
+
else:
|
|
257
|
+
bytes_data += struct.pack(endian + "h", 0)
|
|
258
|
+
|
|
259
|
+
# ADC Event
|
|
260
|
+
if self.adc:
|
|
261
|
+
bytes_data += self.adc.to_bytes(endian)
|
|
262
|
+
else:
|
|
263
|
+
bytes_data += struct.pack(endian + "h", 0)
|
|
264
|
+
|
|
265
|
+
# Trigger Event
|
|
266
|
+
if self.trig:
|
|
267
|
+
bytes_data += self.trig.to_bytes(endian)
|
|
268
|
+
else:
|
|
269
|
+
bytes_data += struct.pack(endian + "h", 0)
|
|
270
|
+
|
|
271
|
+
return bytes_data
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class Segment:
|
|
275
|
+
"""Ceq segment."""
|
|
276
|
+
|
|
277
|
+
def __init__(self, segment_id: int, block_ids: list[int]):
|
|
278
|
+
self.segment_id = segment_id
|
|
279
|
+
self.n_blocks_in_segment = len(block_ids)
|
|
280
|
+
self.block_ids = np.asarray(block_ids, dtype=np.int16)
|
|
281
|
+
|
|
282
|
+
def to_bytes(self, endian=">") -> bytes:
|
|
283
|
+
return (
|
|
284
|
+
struct.pack(endian + "h", self.segment_id)
|
|
285
|
+
+ struct.pack(endian + "h", self.n_blocks_in_segment)
|
|
286
|
+
+ self.block_ids.astype(endian + "i2").tobytes()
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class Ceq:
|
|
291
|
+
"""CEQ structure."""
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
parent_blocks: list[PulseqBlock],
|
|
296
|
+
loop: list[list],
|
|
297
|
+
sections_edges: list[list[int]],
|
|
298
|
+
):
|
|
299
|
+
loop = np.asarray(loop, dtype=np.float32)
|
|
300
|
+
segments = _build_segments(loop, sections_edges)
|
|
301
|
+
|
|
302
|
+
# build CEQ structure
|
|
303
|
+
self.n_max = loop.shape[0]
|
|
304
|
+
self.n_parent_blocks = len(parent_blocks)
|
|
305
|
+
self.n_segments = len(segments)
|
|
306
|
+
self.parent_blocks = parent_blocks
|
|
307
|
+
self.segments = segments
|
|
308
|
+
self.n_columns_in_loop_array = loop.shape[1] - 2 # discard "hasrot", "hasadc"
|
|
309
|
+
self.loop = loop[:, :-2]
|
|
310
|
+
self.max_b1 = _find_b1_max(parent_blocks)
|
|
311
|
+
self.duration = _calc_duration(self.loop[:, 0], self.loop[:, 9])
|
|
312
|
+
self.n_readouts = int(np.sum(loop[:, -2]))
|
|
313
|
+
|
|
314
|
+
def to_bytes(self, endian=">") -> bytes: # noqa
|
|
315
|
+
bytes_data = (
|
|
316
|
+
struct.pack(endian + "i", self.n_max)
|
|
317
|
+
+ struct.pack(endian + "h", self.n_parent_blocks)
|
|
318
|
+
+ struct.pack(endian + "h", self.n_segments)
|
|
319
|
+
)
|
|
320
|
+
for block in self.parent_blocks:
|
|
321
|
+
bytes_data += block.to_bytes(endian)
|
|
322
|
+
for segment in self.segments:
|
|
323
|
+
bytes_data += segment.to_bytes(endian)
|
|
324
|
+
bytes_data += struct.pack(endian + "h", self.n_columns_in_loop_array)
|
|
325
|
+
bytes_data += self.loop.astype(endian + "f4").tobytes()
|
|
326
|
+
bytes_data += struct.pack(endian + "f", self.max_b1)
|
|
327
|
+
bytes_data += struct.pack(endian + "f", self.duration)
|
|
328
|
+
bytes_data += struct.pack(endian + "i", self.n_readouts)
|
|
329
|
+
|
|
330
|
+
return bytes_data
|
|
331
|
+
|
|
332
|
+
def export(self, dformat="file"): # noqa
|
|
333
|
+
if dformat == "bytes":
|
|
334
|
+
return self.to_bytes(endian=">")
|
|
335
|
+
elif dformat == "file":
|
|
336
|
+
return self.to_bytes(endian="<")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@dataclass
|
|
340
|
+
class SequenceParams:
|
|
341
|
+
"""
|
|
342
|
+
Python representation of the C SequenceParams struct.
|
|
343
|
+
|
|
344
|
+
Attributes
|
|
345
|
+
----------
|
|
346
|
+
FOVx : Optional[float]
|
|
347
|
+
Field of view in mm (x-direction).
|
|
348
|
+
FOVy : Optional[float]
|
|
349
|
+
Field of view in mm (y-direction).
|
|
350
|
+
Nx : Optional[int]
|
|
351
|
+
Matrix size (x-direction).
|
|
352
|
+
Ny : Optional[int]
|
|
353
|
+
Matrix size (y-direction).
|
|
354
|
+
Nslices : Optional[int]
|
|
355
|
+
Number of slices.
|
|
356
|
+
Nechoes : Optional[int]
|
|
357
|
+
Number of echoes.
|
|
358
|
+
Nphases : Optional[int]
|
|
359
|
+
Number of phases.
|
|
360
|
+
slice_thickness : Optional[float]
|
|
361
|
+
Thickness of each slice (mm).
|
|
362
|
+
slice_spacing : Optional[float]
|
|
363
|
+
Spacing between slices (mm).
|
|
364
|
+
Rplane : Optional[float]
|
|
365
|
+
In-plane undersampling factor.
|
|
366
|
+
Rplane2 : Optional[float]
|
|
367
|
+
Additional in-plane undersampling factor.
|
|
368
|
+
Rslice : Optional[float]
|
|
369
|
+
Through-plane undersampling factor.
|
|
370
|
+
PFplane : Optional[float]
|
|
371
|
+
In-plane partial fourier.
|
|
372
|
+
PFslice : Optional[float]
|
|
373
|
+
Through-plane partial fourier.
|
|
374
|
+
ETL : Optional[int]
|
|
375
|
+
Number of k-space shots per readout.
|
|
376
|
+
TE : Optional[float]
|
|
377
|
+
Echo time (ms).
|
|
378
|
+
TE0 : Optional[float]
|
|
379
|
+
First echo time (ms) for multiecho.
|
|
380
|
+
TR : Optional[float]
|
|
381
|
+
Repetition time (ms).
|
|
382
|
+
Tprep : Optional[float]
|
|
383
|
+
Preparation time (ms).
|
|
384
|
+
Trecovery : Optional[float]
|
|
385
|
+
Recovery time (ms).
|
|
386
|
+
flip : Optional[float]
|
|
387
|
+
Flip angle in degrees.
|
|
388
|
+
flip2 : Optional[float]
|
|
389
|
+
Second flip angle in degrees.
|
|
390
|
+
refoc_flip : Optional[float]
|
|
391
|
+
Refocusing flip angle in degrees.
|
|
392
|
+
freq_dir : Optional[int]
|
|
393
|
+
Frequency direction (0: A/P; 1: S/L).
|
|
394
|
+
freq_verse : Optional[int]
|
|
395
|
+
Frequency verse (1: normal; -1: swapped).
|
|
396
|
+
phase_verse : Optional[int]
|
|
397
|
+
Phase verse (1: normal; -1: swapped).
|
|
398
|
+
bipolar_echoes : Optional[int]
|
|
399
|
+
Bipolar echoes (0: false, 1: true).
|
|
400
|
+
dwell : Optional[float]
|
|
401
|
+
ADC dwell time (s).
|
|
402
|
+
raster : Optional[float]
|
|
403
|
+
Waveform raster time (s).
|
|
404
|
+
gmax : Optional[float]
|
|
405
|
+
Maximum gradient strength (mT/m).
|
|
406
|
+
smax : Optional[float]
|
|
407
|
+
Maximum gradient slew rate (T/m/s).
|
|
408
|
+
b1_max : Optional[float]
|
|
409
|
+
Maximum RF value (uT).
|
|
410
|
+
b0_field : Optional[float]
|
|
411
|
+
System field strength (T).
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
FOVx: float | None = None
|
|
415
|
+
FOVy: float | None = None
|
|
416
|
+
Nx: int | None = None
|
|
417
|
+
Ny: int | None = None
|
|
418
|
+
Nslices: int | None = None
|
|
419
|
+
Nechoes: int | None = None
|
|
420
|
+
Nphases: int | None = None
|
|
421
|
+
slice_thickness: float | None = None
|
|
422
|
+
slice_spacing: float | None = None
|
|
423
|
+
Rplane: float | None = None
|
|
424
|
+
Rplane2: float | None = None
|
|
425
|
+
Rslice: float | None = None
|
|
426
|
+
PFplane: float | None = None
|
|
427
|
+
PFslice: float | None = None
|
|
428
|
+
ETL: int | None = None
|
|
429
|
+
TE: float | None = None
|
|
430
|
+
TE0: float | None = None
|
|
431
|
+
TR: float | None = None
|
|
432
|
+
Tprep: float | None = None
|
|
433
|
+
Trecovery: float | None = None
|
|
434
|
+
flip: float | None = None
|
|
435
|
+
flip2: float | None = None
|
|
436
|
+
refoc_flip: float | None = None
|
|
437
|
+
freq_dir: int | None = None
|
|
438
|
+
freq_verse: int | None = None
|
|
439
|
+
phase_verse: int | None = None
|
|
440
|
+
bipolar_echoes: int | None = None
|
|
441
|
+
dwell: float | None = None
|
|
442
|
+
raster: float | None = None
|
|
443
|
+
gmax: float | None = None
|
|
444
|
+
smax: float | None = None
|
|
445
|
+
b1_max: float | None = None
|
|
446
|
+
b0_field: float | None = None
|
|
447
|
+
|
|
448
|
+
@classmethod
|
|
449
|
+
def from_bytes(cls, data: bytes) -> "SequenceParams":
|
|
450
|
+
"""
|
|
451
|
+
Deserialize from a byte array into a SequenceParams object.
|
|
452
|
+
|
|
453
|
+
Parameters
|
|
454
|
+
----------
|
|
455
|
+
data : bytes
|
|
456
|
+
A byte array representing a serialized SequenceParams object.
|
|
457
|
+
|
|
458
|
+
Returns
|
|
459
|
+
-------
|
|
460
|
+
SequenceParams
|
|
461
|
+
A SequenceParams instance with attributes filled.
|
|
462
|
+
"""
|
|
463
|
+
format_string = "2f 5h 2f 5f h 5f 3f 3h h 6f"
|
|
464
|
+
unpacked = struct.unpack(format_string, data)
|
|
465
|
+
|
|
466
|
+
# Replace -1 values with None
|
|
467
|
+
unpacked = [None if x == -1 or x == -1.0 else x for x in unpacked]
|
|
468
|
+
|
|
469
|
+
return cls(*unpacked)
|
|
470
|
+
|
|
471
|
+
def asdict(self) -> dict:
|
|
472
|
+
"""
|
|
473
|
+
Return a dictionary of the dataclass, excluding None values.
|
|
474
|
+
|
|
475
|
+
Returns
|
|
476
|
+
-------
|
|
477
|
+
dict
|
|
478
|
+
A dictionary of the dataclass fields, excluding None values.
|
|
479
|
+
"""
|
|
480
|
+
return {k: v for k, v in _asdict(self).items() if v is not None}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# %% local subroutines
|
|
484
|
+
def _build_segments(loop, sections_edges):
|
|
485
|
+
hasrot = np.ascontiguousarray(loop[:, -1]).astype(int)
|
|
486
|
+
parent_block_id = np.ascontiguousarray(loop[:, 1]).astype(int) * hasrot
|
|
487
|
+
|
|
488
|
+
# build section edges
|
|
489
|
+
if not sections_edges:
|
|
490
|
+
sections_edges = [0]
|
|
491
|
+
sections_edges = np.stack((sections_edges, sections_edges[1:] + [-1]), axis=-1)
|
|
492
|
+
|
|
493
|
+
# loop over sections and find segment definitions
|
|
494
|
+
segment_id = np.zeros(loop.shape[0], dtype=np.float32)
|
|
495
|
+
seg_definitions = []
|
|
496
|
+
|
|
497
|
+
# fill sections from 0 to n-1
|
|
498
|
+
n_sections = len(sections_edges)
|
|
499
|
+
for n in range(n_sections - 1):
|
|
500
|
+
section_start, section_end = sections_edges[n]
|
|
501
|
+
_seg_definition = _autoseg.find_segment_definitions(
|
|
502
|
+
parent_block_id[section_start:section_end]
|
|
503
|
+
)
|
|
504
|
+
_seg_definition = _autoseg.split_rotated_segments(_seg_definition)
|
|
505
|
+
seg_definitions.extend(_seg_definition)
|
|
506
|
+
|
|
507
|
+
# fill last section
|
|
508
|
+
section_start = sections_edges[-1][0]
|
|
509
|
+
_seg_definition = _autoseg.find_segment_definitions(parent_block_id[section_start:])
|
|
510
|
+
_seg_definition = _autoseg.split_rotated_segments(_seg_definition)
|
|
511
|
+
seg_definitions.extend(_seg_definition)
|
|
512
|
+
|
|
513
|
+
# for each block, find the segment it belongs to
|
|
514
|
+
for n in range(len(seg_definitions)):
|
|
515
|
+
idx = _autoseg.find_segments(parent_block_id, seg_definitions[n])
|
|
516
|
+
segment_id[idx] = n
|
|
517
|
+
loop[:, 0] = segment_id
|
|
518
|
+
|
|
519
|
+
# now build segment fields
|
|
520
|
+
n_segments = len(seg_definitions)
|
|
521
|
+
segments = []
|
|
522
|
+
for n in range(n_segments):
|
|
523
|
+
segments.append(Segment(n, seg_definitions[n]))
|
|
524
|
+
|
|
525
|
+
return segments
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _find_b1_max(parent_blocks):
|
|
529
|
+
return np.max(
|
|
530
|
+
[block.rf.wav.amplitude for block in parent_blocks if block.rf is not None]
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _calc_duration(segment_id, block_duration):
|
|
535
|
+
block_duration = block_duration.sum()
|
|
536
|
+
|
|
537
|
+
# total segment ringdown
|
|
538
|
+
n_seg_boundaries = (np.diff(segment_id) != 0).sum()
|
|
539
|
+
seg_ringdown_duration = SEGMENT_RINGDOWN_TIME * n_seg_boundaries
|
|
540
|
+
|
|
541
|
+
return block_duration + seg_ringdown_duration
|