mrzerocore 0.4.3__cp37-abi3-musllinux_1_2_aarch64.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.
- MRzeroCore/__init__.py +22 -0
- MRzeroCore/_prepass.abi3.so +0 -0
- MRzeroCore/phantom/brainweb/.gitignore +1 -0
- MRzeroCore/phantom/brainweb/__init__.py +192 -0
- MRzeroCore/phantom/brainweb/brainweb_data.json +92 -0
- MRzeroCore/phantom/brainweb/brainweb_data_sources.txt +74 -0
- MRzeroCore/phantom/brainweb/output/.gitkeep +0 -0
- MRzeroCore/phantom/custom_voxel_phantom.py +240 -0
- MRzeroCore/phantom/nifti_phantom.py +210 -0
- MRzeroCore/phantom/sim_data.py +200 -0
- MRzeroCore/phantom/tissue_dict.py +269 -0
- MRzeroCore/phantom/voxel_grid_phantom.py +610 -0
- MRzeroCore/pulseq/exporter.py +374 -0
- MRzeroCore/pulseq/exporter_v2.py +650 -0
- MRzeroCore/pulseq/helpers.py +228 -0
- MRzeroCore/pulseq/pulseq_exporter.py +553 -0
- MRzeroCore/pulseq/pulseq_loader/__init__.py +66 -0
- MRzeroCore/pulseq/pulseq_loader/adc.py +48 -0
- MRzeroCore/pulseq/pulseq_loader/helpers.py +75 -0
- MRzeroCore/pulseq/pulseq_loader/pulse.py +80 -0
- MRzeroCore/pulseq/pulseq_loader/pulseq_file/__init__.py +235 -0
- MRzeroCore/pulseq/pulseq_loader/pulseq_file/adc.py +68 -0
- MRzeroCore/pulseq/pulseq_loader/pulseq_file/block.py +98 -0
- MRzeroCore/pulseq/pulseq_loader/pulseq_file/definitons.py +68 -0
- MRzeroCore/pulseq/pulseq_loader/pulseq_file/gradient.py +70 -0
- MRzeroCore/pulseq/pulseq_loader/pulseq_file/helpers.py +156 -0
- MRzeroCore/pulseq/pulseq_loader/pulseq_file/rf.py +91 -0
- MRzeroCore/pulseq/pulseq_loader/pulseq_file/trap.py +69 -0
- MRzeroCore/pulseq/pulseq_loader/spoiler.py +33 -0
- MRzeroCore/reconstruction.py +104 -0
- MRzeroCore/sequence.py +747 -0
- MRzeroCore/simulation/isochromat_sim.py +254 -0
- MRzeroCore/simulation/main_pass.py +286 -0
- MRzeroCore/simulation/pre_pass.py +192 -0
- MRzeroCore/simulation/sig_to_mrd.py +362 -0
- MRzeroCore/util.py +884 -0
- MRzeroCore.libs/libgcc_s-39080030.so.1 +0 -0
- mrzerocore-0.4.3.dist-info/METADATA +121 -0
- mrzerocore-0.4.3.dist-info/RECORD +41 -0
- mrzerocore-0.4.3.dist-info/WHEEL +4 -0
- mrzerocore-0.4.3.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
import sys
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
from . import sequence as Seq
|
|
7
|
+
from . import util
|
|
8
|
+
|
|
9
|
+
sys.path.append("../scannerloop_libs")
|
|
10
|
+
from pypulseq.Sequence.sequence import Sequence
|
|
11
|
+
from pypulseq.make_adc import make_adc as _make_adc
|
|
12
|
+
from pypulseq.make_delay import make_delay as _make_delay
|
|
13
|
+
from pypulseq.make_sinc_pulse import make_sinc_pulse
|
|
14
|
+
from pypulseq.make_trap_pulse import make_trapezoid as _make_trapezoid
|
|
15
|
+
from pypulseq.make_gauss_pulse import make_gauss_pulse
|
|
16
|
+
from pypulseq.make_block_pulse import make_block_pulse
|
|
17
|
+
from pypulseq.opts import Opts
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def rectify_flips(flips):
|
|
21
|
+
flip_angle = flips.angle.cpu()
|
|
22
|
+
flip_phase = flips.phase.cpu()
|
|
23
|
+
|
|
24
|
+
if flips.angle < 0:
|
|
25
|
+
flip_angle = -flips.angle
|
|
26
|
+
flip_phase = flips.phase + np.pi
|
|
27
|
+
flip_phase = torch.fmod(flip_phase, 2*np.pi)
|
|
28
|
+
return flip_angle.item(), flip_phase.item()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Modified versions of make_delay, make_adc and make_trapezoid that ensure that
|
|
32
|
+
# all events (and thus gradients) are on the gradient time raster. If they are
|
|
33
|
+
# not, the scanner crashes without hinting why
|
|
34
|
+
|
|
35
|
+
def make_delay(d: float) -> SimpleNamespace:
|
|
36
|
+
"""make_delay wrapper that rounds delay to the gradient time raster."""
|
|
37
|
+
return _make_delay(round(d / 10e-6) * 10e-6)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def make_adc(num_samples: int, system: Opts = Opts(), dwell: float = 0, duration: float = 0, delay: float = 0,
|
|
41
|
+
freq_offset: float = 0, phase_offset: float = 0) -> SimpleNamespace:
|
|
42
|
+
"""make_adc wrapper that modifies the delay such that the total duration
|
|
43
|
+
is on the gradient time raster."""
|
|
44
|
+
# TODO: the total duration might not be on the gradient raster. If a
|
|
45
|
+
# sequence with optimized ADC durations fails the timing check, implement
|
|
46
|
+
# this functions to round the timing as necessary.
|
|
47
|
+
|
|
48
|
+
return _make_adc(
|
|
49
|
+
num_samples, system, dwell, duration, delay,
|
|
50
|
+
freq_offset, phase_offset
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def make_trapezoid(channel: str, amplitude: float = 0, area: float = None, delay: float = 0, duration: float = 0,
|
|
55
|
+
flat_area: float = 0, flat_time: float = -1, max_grad: float = 0, max_slew: float = 0,
|
|
56
|
+
rise_time: float = 0, system: Opts = Opts()) -> SimpleNamespace:
|
|
57
|
+
"""make_trapezoid wrapper that rounds gradients to the raster."""
|
|
58
|
+
raster = system.grad_raster_time
|
|
59
|
+
if delay != -1:
|
|
60
|
+
delay = round(delay / raster) * raster
|
|
61
|
+
if rise_time != -1:
|
|
62
|
+
rise_time = round(rise_time / raster) * raster
|
|
63
|
+
if flat_time != -1:
|
|
64
|
+
flat_time = round(flat_time / raster) * raster
|
|
65
|
+
if duration != -1:
|
|
66
|
+
duration = round(duration / raster) * raster
|
|
67
|
+
|
|
68
|
+
return _make_trapezoid(
|
|
69
|
+
channel, amplitude, area, delay, duration, flat_area, flat_time,
|
|
70
|
+
max_grad, max_slew, rise_time, system
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
nonsel = 0
|
|
75
|
+
|
|
76
|
+
def pulseq_write_EPG_3D(seq_param: Sequence,
|
|
77
|
+
path: str,
|
|
78
|
+
FOV: tuple[float, float, float],
|
|
79
|
+
plot_seq: bool = False,
|
|
80
|
+
write_data: int = 1):
|
|
81
|
+
|
|
82
|
+
"""Export Sequence as pulseq file Version 1.3.1post1.
|
|
83
|
+
|
|
84
|
+
This function creates a pulseq file where the exported seq files matches
|
|
85
|
+
the simulation.
|
|
86
|
+
The SEQ file undergoes several modifications.
|
|
87
|
+
Firstly, the center of a pulse is placed at the beginning of the repetition.
|
|
88
|
+
To achieve this, the time of the last event within a repetition is reduced
|
|
89
|
+
by half the pulse length.
|
|
90
|
+
The gradient moment of the adc ramp (rising) is addtionally substracted
|
|
91
|
+
from the gradient in the event before an adc!
|
|
92
|
+
The gradient moment of the adc ramp (falling) is addtionally substracted
|
|
93
|
+
from the gradient in the event after an adc!
|
|
94
|
+
The simulation needs a rewinder of N_adc/2-1, since first a gradient is
|
|
95
|
+
played out, than the adc point is measured. To match this to the exporter
|
|
96
|
+
an additional half gradient moment of one adc needs to be added to the gradient
|
|
97
|
+
before and after an adc!
|
|
98
|
+
# Example for resolution 8! Rewinder goes to -5, after adc the gradient moment is +3.
|
|
99
|
+
# ADC wrong index / -4.5 -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 \
|
|
100
|
+
# Gradient Moment / -5 -4 -3 -2 -1 +0 +1 +2 +3 \
|
|
101
|
+
# Correct Gradient before adc by +deltakx/2!
|
|
102
|
+
# Correct Gradient after adc by -deltakx/2!
|
|
103
|
+
# ADC correct index / -4 -3 -2 -1 +0 +1 +2 +3 \
|
|
104
|
+
# Gradient Moment / -4.5 -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 +3.5 \
|
|
105
|
+
|
|
106
|
+
Warnings
|
|
107
|
+
--------
|
|
108
|
+
The exporter only operates successfully when the phase-encoding gradient
|
|
109
|
+
directly precedes the ADC.
|
|
110
|
+
Furthermore, gradients must also played out directly in the event after the ADCs,
|
|
111
|
+
so that they can be handled correctly.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
seq_param : Sequence
|
|
116
|
+
Sequence that will be exported as pulseq file.
|
|
117
|
+
path : String
|
|
118
|
+
Location where the pulseq file is saved.
|
|
119
|
+
FOV : tuple[float, float, float]
|
|
120
|
+
FOV of the sequence.
|
|
121
|
+
plot_seq : bool
|
|
122
|
+
Plot the sequences.
|
|
123
|
+
write_data : int
|
|
124
|
+
Writes the encoding of the sequence as command into the pulseq file.
|
|
125
|
+
This is needed for easier reconstruction of measurements.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
No return!
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
bdw_start = 0
|
|
135
|
+
|
|
136
|
+
# save pulseq definition
|
|
137
|
+
# slice_thickness = np.max([8e-3,5e-3*num_slices])
|
|
138
|
+
MAXSLEW = 200
|
|
139
|
+
FOV = [item / 1000 for item in FOV]
|
|
140
|
+
deltakx = 1.0 / FOV[0] # /(2*np.pi) before v.2.1.0
|
|
141
|
+
deltaky = 1.0 / FOV[1]
|
|
142
|
+
deltakz = 1.0 / FOV[2]
|
|
143
|
+
|
|
144
|
+
kwargs_for_opts = {"rf_ringdown_time": 20e-6, "rf_dead_time": 100e-6, "adc_dead_time": 20e-6, "max_grad": 80, "grad_unit": "mT/m", "max_slew": MAXSLEW, "slew_unit": "T/m/s"}
|
|
145
|
+
system = Opts(**kwargs_for_opts)
|
|
146
|
+
seq = Sequence(system)
|
|
147
|
+
|
|
148
|
+
seq.set_definition("FOV", FOV)
|
|
149
|
+
seq.add_block(make_delay(5.0))
|
|
150
|
+
|
|
151
|
+
# import pdb; pdb.set_trace()
|
|
152
|
+
for i,rep in enumerate(seq_param): # Loop over all repetitions
|
|
153
|
+
adc_start = 0
|
|
154
|
+
flip_angle,flip_phase = rectify_flips(rep.pulse)
|
|
155
|
+
|
|
156
|
+
for event in range(rep.event_count): # Loop over all events within one repetitions
|
|
157
|
+
|
|
158
|
+
# No adc in event
|
|
159
|
+
if torch.abs(rep.adc_usage[event]) == 0:
|
|
160
|
+
|
|
161
|
+
RFdurafter = 0
|
|
162
|
+
RFdurbefore = 0
|
|
163
|
+
|
|
164
|
+
# First event
|
|
165
|
+
if event == 0:
|
|
166
|
+
|
|
167
|
+
###############################
|
|
168
|
+
## global pulse
|
|
169
|
+
if rep.pulse.usage == Seq.PulseUsage.UNDEF:
|
|
170
|
+
RFdur = 0
|
|
171
|
+
if torch.abs(rep.pulse.angle) > 1e-8:
|
|
172
|
+
|
|
173
|
+
RFdur = 1*1e-3
|
|
174
|
+
kwargs_for_block = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "phase_offset": flip_phase}
|
|
175
|
+
rf_ex = make_block_pulse(**kwargs_for_block)
|
|
176
|
+
|
|
177
|
+
seq.add_block(rf_ex)
|
|
178
|
+
seq.add_block(make_delay(1e-4))
|
|
179
|
+
|
|
180
|
+
RFdurafter = 1e-4 + RFdur/2 + rf_ex.ringdown_time
|
|
181
|
+
|
|
182
|
+
elif (rep.pulse.usage == Seq.PulseUsage.EXCIT or
|
|
183
|
+
rep.pulse.usage == Seq.PulseUsage.STORE):
|
|
184
|
+
###############################
|
|
185
|
+
### excitation pulse
|
|
186
|
+
RFdur = 0
|
|
187
|
+
if torch.abs(rep.pulse.angle) > 1e-8:
|
|
188
|
+
|
|
189
|
+
if nonsel:
|
|
190
|
+
RFdur = 1*1e-3
|
|
191
|
+
kwargs_for_block = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "phase_offset": flip_phase}
|
|
192
|
+
rf_ex = make_block_pulse(**kwargs_for_block)
|
|
193
|
+
seq.add_block(rf_ex)
|
|
194
|
+
seq.add_block(make_delay(1e-4))
|
|
195
|
+
|
|
196
|
+
RFdurafter = 1e-4 + RFdur/2 + rf_ex.ringdown_time
|
|
197
|
+
|
|
198
|
+
else:
|
|
199
|
+
# alternatively slice selective:
|
|
200
|
+
RFdur = 1*1e-3
|
|
201
|
+
#kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": slice_thickness, "apodization": 0.5, "time_bw_product": 4, "phase_offset": flip_phase}
|
|
202
|
+
#rf_ex, gz, gzr= make_sinc_pulse(**kwargs_for_sinc)
|
|
203
|
+
kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": FOV[2], "apodization": 0.15, "time_bw_product": 2, "phase_offset": flip_phase, "return_gz": True}
|
|
204
|
+
rf_ex, gz, gzr = make_sinc_pulse(**kwargs_for_sinc)
|
|
205
|
+
|
|
206
|
+
#satPulse = mr.makeSincPulse(fa_sat, 'Duration', tp, 'system', seq.sys,'timeBwProduct', 2,'apodization', 0.15); % philips-like sinc
|
|
207
|
+
#%satPulse = mr.makeGaussPulse(fa_sat, 'Duration', t_p,'system',lims,'timeBwProduct', 0.2,'apodization', 0.5); % siemens-like gauss
|
|
208
|
+
|
|
209
|
+
seq.add_block(gzr)
|
|
210
|
+
seq.add_block(rf_ex, gz)
|
|
211
|
+
seq.add_block(gzr)
|
|
212
|
+
|
|
213
|
+
RFdurafter = gz.flat_time/2 + gz.fall_time + gzr.rise_time + gzr.flat_time + gzr.fall_time
|
|
214
|
+
|
|
215
|
+
elif rep.pulse.usage == Seq.PulseUsage.REFOC:
|
|
216
|
+
###############################
|
|
217
|
+
### refocusing pulse
|
|
218
|
+
RFdur = 0
|
|
219
|
+
|
|
220
|
+
if torch.abs(rep.pulse.angle) > 1e-8:
|
|
221
|
+
RFdur = 1*1e-3
|
|
222
|
+
|
|
223
|
+
if nonsel:
|
|
224
|
+
kwargs_for_block = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "phase_offset": flip_phase}
|
|
225
|
+
rf_ref = make_block_pulse(**kwargs_for_block)
|
|
226
|
+
seq.add_block(rf_ref)
|
|
227
|
+
seq.add_block(make_delay(1e-4))
|
|
228
|
+
|
|
229
|
+
RFdurafter = 1e-4 + RFdur/2 + rf_ref.ringdown_time
|
|
230
|
+
else:
|
|
231
|
+
|
|
232
|
+
kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": FOV[2], "apodization": 0.5, "time_bw_product": 4, "phase_offset": flip_phase, "return_gz": True}
|
|
233
|
+
rf_ref, gz_ref, gzr = make_sinc_pulse(**kwargs_for_sinc)
|
|
234
|
+
seq.add_block(gzr)
|
|
235
|
+
seq.add_block(rf_ref, gz_ref)
|
|
236
|
+
seq.add_block(gzr)
|
|
237
|
+
|
|
238
|
+
RFdurafter = gz_ref.flat_time/2 + gz_ref.fall_time + gzr.rise_time + gzr.flat_time + gzr.fall_time
|
|
239
|
+
|
|
240
|
+
elif rep.pulse.usage == Seq.PulseUsage.FATSAT:
|
|
241
|
+
###############################
|
|
242
|
+
### fat saturation pulse
|
|
243
|
+
RFdur = 6.120*1e-3
|
|
244
|
+
dCurrFrequency = 123.2
|
|
245
|
+
kwargs_for_gauss = {"flip_angle": 110*np.pi/180, "system": system, "slice_thickness": FOV[2], "duration": RFdur, "freq_offset": -3.3*dCurrFrequency, "time_bw_product": 0.2, "apodization": 0.5, "return_gz": True} # "bandwidth": 200
|
|
246
|
+
rf_ex, gz, gzr = make_gauss_pulse(**kwargs_for_gauss)
|
|
247
|
+
seq.add_block(gzr)
|
|
248
|
+
seq.add_block(rf_ex, gz)
|
|
249
|
+
seq.add_block(gzr)
|
|
250
|
+
|
|
251
|
+
RFdurafter = gz.flat_time/2 + gz.fall_time + gzr.rise_time + gzr.flat_time + gzr.fall_time
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# Last event in one repetition, except the last repetition at all!
|
|
255
|
+
# Calculation of RF duration of the following repetition
|
|
256
|
+
|
|
257
|
+
elif event == rep.event_count - 1 and i < len(seq_param) - 2:
|
|
258
|
+
|
|
259
|
+
if seq_param[i+1].pulse.usage == Seq.PulseUsage.UNDEF or nonsel:
|
|
260
|
+
RFdur = 0
|
|
261
|
+
if torch.abs(rep.pulse.angle) > 1e-8:
|
|
262
|
+
RFdur = 1*1e-3
|
|
263
|
+
kwargs_for_block = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "phase_offset": flip_phase}
|
|
264
|
+
rf_ex = make_block_pulse(**kwargs_for_block)
|
|
265
|
+
RFdurbefore = rf_ex.delay + RFdur/2
|
|
266
|
+
|
|
267
|
+
elif (seq_param[i+1].pulse.usage == Seq.PulseUsage.EXCIT or
|
|
268
|
+
seq_param[i+1].pulse.usage == Seq.PulseUsage.STORE):
|
|
269
|
+
RFdur = 0
|
|
270
|
+
if torch.abs(rep.pulse.angle) > 1e-8:
|
|
271
|
+
RFdur = 1*1e-3
|
|
272
|
+
kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": FOV[2], "apodization": 0.15, "time_bw_product": 2, "phase_offset": flip_phase, "return_gz": True}
|
|
273
|
+
rf_ex, gz, gzr = make_sinc_pulse(**kwargs_for_sinc)
|
|
274
|
+
RFdurbefore = gzr.rise_time + gzr.flat_time + gzr.fall_time + gz.delay + gz.rise_time + gz.flat_time/2
|
|
275
|
+
|
|
276
|
+
elif seq_param[i+1].pulse.usage == Seq.PulseUsage.REFOC:
|
|
277
|
+
RFdur = 0
|
|
278
|
+
if torch.abs(rep.pulse.angle) > 1e-8:
|
|
279
|
+
RFdur = 1*1e-3
|
|
280
|
+
kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": FOV[2], "apodization": 0.5, "time_bw_product": 4, "phase_offset": flip_phase, "return_gz": True}
|
|
281
|
+
rf_ref, gz_ref, gzr = make_sinc_pulse(**kwargs_for_sinc)
|
|
282
|
+
RFdurbefore = gzr.rise_time + gzr.flat_time + gzr.fall_time + gz_ref.delay + gz_ref.rise_time + gz_ref.flat_time/2
|
|
283
|
+
|
|
284
|
+
elif seq_param[i+1].pulse.usage == Seq.PulseUsage.FATSAT:
|
|
285
|
+
###############################
|
|
286
|
+
### fat saturation pulse
|
|
287
|
+
RFdur = 6.120*1e-3
|
|
288
|
+
dCurrFrequency = 123.2
|
|
289
|
+
kwargs_for_gauss = {"flip_angle": 110*np.pi/180, "system": system, "slice_thickness": FOV[2], "duration": RFdur, "freq_offset": -3.3*dCurrFrequency, "time_bw_product": 0.2, "apodization": 0.5, "return_gz": True} # "bandwidth": 200
|
|
290
|
+
rf_ex, gz, gzr= make_gauss_pulse(**kwargs_for_gauss)
|
|
291
|
+
RFdurbefore = gzr.rise_time + gzr.flat_time + gzr.fall_time + gz.delay + gz.rise_time + gz.flat_time/2
|
|
292
|
+
|
|
293
|
+
# Substract the time of the rf pulse starting from the middle of the rf pulse to match simulation
|
|
294
|
+
dur = rep.event_time[event].item() - RFdurafter
|
|
295
|
+
|
|
296
|
+
# Substract the time of the rf pulse of the following repetition
|
|
297
|
+
if event == rep.event_count - 1: # Last event in rep
|
|
298
|
+
dur -= RFdurbefore
|
|
299
|
+
|
|
300
|
+
if dur < 0:
|
|
301
|
+
raise Exception('Event Time too short! Event Time: Rep: '+ str(i) + ', Event: ' +str(event),', increase event_time by at least: ' + str(-dur))
|
|
302
|
+
|
|
303
|
+
gx_gradmom = rep.gradm[event,0].item()*deltakx
|
|
304
|
+
gy_gradmom = rep.gradm[event,1].item()*deltaky
|
|
305
|
+
gz_gradmom = rep.gradm[event,2].item()*deltakz
|
|
306
|
+
idx_T = np.nonzero(torch.abs(rep.adc_usage))
|
|
307
|
+
dur_adc = np.round(torch.sum(rep.event_time[idx_T],0).item(),decimals=5)
|
|
308
|
+
|
|
309
|
+
if np.abs(gx_gradmom)>0:
|
|
310
|
+
gx_adc_ramp = 0
|
|
311
|
+
if any(rep.adc_usage):
|
|
312
|
+
# Case: Gradient before adc
|
|
313
|
+
# Correct duration by the rise time
|
|
314
|
+
# Correct gradient by the gradient moment of adc ramp
|
|
315
|
+
if event < rep.event_count - 1 and torch.abs(rep.adc_usage[event+1]) and torch.abs(rep.gradm[event+1,0]) > 0:
|
|
316
|
+
|
|
317
|
+
# ADC wrong / -4.5 -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 \
|
|
318
|
+
# Gradient Moment / -5 -4 -3 -2 -1 +0 +1 +2 +3 \
|
|
319
|
+
# Correct Gradient before adc by +0.5 --> neg sign needed because the gradient correction is substracted (gx_gradmom-gx_adc_ramp)!
|
|
320
|
+
# Correct Gradient after adc by -0.5 --> pos sign is needed because the gradient correction is substracted (gx_gradmom-gx_adc_ramp)!
|
|
321
|
+
# ADC correct / -4 -3 -2 -1 +0 +1 +2 +3 \
|
|
322
|
+
# Gradient Moment / -4.5 -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 +3.5 \
|
|
323
|
+
|
|
324
|
+
kwargs_for_gx = {"channel": 'x', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,0]).item()*deltakx, "flat_time": dur_adc}
|
|
325
|
+
gx_adc = make_trapezoid(**kwargs_for_gx)
|
|
326
|
+
gx_adc_ramp = gx_adc.amplitude*gx_adc.rise_time/2 - torch.sign(rep.gradm[event+1,0]).item() * deltakx/2 # Correct gradient by half of the gradient moment of one adc moment!
|
|
327
|
+
gx_rise_time = gx_adc.rise_time
|
|
328
|
+
dur -= gx_rise_time # Correct timing for rise and fall time of adc gradient
|
|
329
|
+
# Case: Gradient after adc
|
|
330
|
+
# Correct duration by the fall time
|
|
331
|
+
# Correct gradient by the gradient moment of adc ramp
|
|
332
|
+
elif event > 0 and torch.abs(rep.adc_usage[event-1]) and torch.abs(rep.gradm[event-1,0]) > 0: # Event after adc to reduce dur by fall time of adc gradient
|
|
333
|
+
kwargs_for_gx = {"channel": 'x', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,0]).item()*deltakx, "flat_time": dur_adc}
|
|
334
|
+
gx_adc = make_trapezoid(**kwargs_for_gx)
|
|
335
|
+
gx_adc_ramp = gx_adc.amplitude*gx_adc.fall_time/2 + torch.sign(rep.gradm[event-1,0]).item() * deltakx/2
|
|
336
|
+
gx_fall_time = gx_adc.fall_time
|
|
337
|
+
dur -= gx_fall_time # Correct timing for rise and fall time of adc gradient
|
|
338
|
+
kwargs_for_gx = {"channel": 'x', "system": system, "area": gx_gradmom-gx_adc_ramp, "duration": dur}
|
|
339
|
+
try:
|
|
340
|
+
gx = make_trapezoid(**kwargs_for_gx)
|
|
341
|
+
except:
|
|
342
|
+
raise Exception('Event Time too short (gx)! Event Time: Rep: ' + str(i) + ', Event: ' +str(event))
|
|
343
|
+
if np.abs(gy_gradmom)>0:
|
|
344
|
+
gy_adc_ramp = 0
|
|
345
|
+
if any(rep.adc_usage):
|
|
346
|
+
# Case: Gradient before adc
|
|
347
|
+
# Correct duration by the rise time
|
|
348
|
+
# Correct gradient by the gradient moment of adc ramp
|
|
349
|
+
if event < rep.event_count - 1 and torch.abs(rep.adc_usage[event+1]) and torch.abs(rep.gradm[event+1,1]) > 0:
|
|
350
|
+
kwargs_for_gy = {"channel": 'y', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,1]).item()*deltaky, "flat_time": dur_adc}
|
|
351
|
+
gy_adc = make_trapezoid(**kwargs_for_gy)
|
|
352
|
+
gy_adc_ramp = gy_adc.amplitude*gy_adc.rise_time/2 - torch.sign(rep.gradm[event+1,1]).item() * deltaky/2
|
|
353
|
+
gy_rise_time = gy_adc.rise_time
|
|
354
|
+
dur -= gy_rise_time # Correct timing for rise and fall time of adc gradient
|
|
355
|
+
# Case: Gradient after adc
|
|
356
|
+
# Correct duration by the fall time
|
|
357
|
+
# Correct gradient by the gradient moment of adc ramp
|
|
358
|
+
elif event > 0 and torch.abs(rep.adc_usage[event-1]) and torch.abs(rep.gradm[event-1,1]) > 0: # Event after adc to reduce dur by fall time of adc gradient
|
|
359
|
+
kwargs_for_gy = {"channel": 'y', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,1]).item()*deltaky, "flat_time": dur_adc}
|
|
360
|
+
gy_adc = make_trapezoid(**kwargs_for_gy)
|
|
361
|
+
gy_adc_ramp = gy_adc.amplitude*gy_adc.fall_time/2 + torch.sign(rep.gradm[event-1,1]).item() * deltaky/2
|
|
362
|
+
gy_fall_time = gy_adc.fall_time
|
|
363
|
+
dur -= gy_fall_time # Correct timing for rise and fall time of adc gradient
|
|
364
|
+
kwargs_for_gy = {"channel": 'y', "system": system, "area": gy_gradmom-gy_adc_ramp, "duration": dur}
|
|
365
|
+
try:
|
|
366
|
+
gy = make_trapezoid(**kwargs_for_gy)
|
|
367
|
+
except:
|
|
368
|
+
raise Exception('Event Time too short (gy)! Event Time: Rep: ' + str(i) + ', Event: ' +str(event))
|
|
369
|
+
if np.abs(gz_gradmom)>0:
|
|
370
|
+
gz_adc_ramp = 0
|
|
371
|
+
if any(rep.adc_usage):
|
|
372
|
+
# Case: Gradient before adc
|
|
373
|
+
# Correct duration by the rise time
|
|
374
|
+
# Correct gradient by the gradient moment of adc ramp
|
|
375
|
+
if event < rep.event_count - 1 and torch.abs(rep.adc_usage[event+1]) and torch.abs(rep.gradm[event+1,2]) > 0:
|
|
376
|
+
kwargs_for_gz = {"channel": 'z', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,2]).item()*deltakz, "flat_time": dur_adc}
|
|
377
|
+
gz_adc = make_trapezoid(**kwargs_for_gz)
|
|
378
|
+
gz_adc_ramp = gz_adc.amplitude*gz_adc.rise_time/2 - torch.sign(rep.gradm[event+1,2]).item() * deltakz/2
|
|
379
|
+
gz_rise_time = gz_adc.rise_time
|
|
380
|
+
dur -= gz_rise_time # Correct timing for rise and fall time of adc gradient
|
|
381
|
+
# Case: Gradient after adc
|
|
382
|
+
# Correct duration by the fall time
|
|
383
|
+
# Correct gradient by the gradient moment of adc ramp
|
|
384
|
+
elif event > 0 and torch.abs(rep.adc_usage[event-1]) and torch.abs(rep.gradm[event-1,2]) > 0: # Event after adc to reduce dur by fall time of adc gradient
|
|
385
|
+
kwargs_for_gz = {"channel": 'z', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,2]).item()*deltakz, "flat_time": dur_adc}
|
|
386
|
+
gz_adc = make_trapezoid(**kwargs_for_gz)
|
|
387
|
+
gz_adc_ramp = gz_adc.amplitude*gz_adc.fall_time/2 + torch.sign(rep.gradm[event-1,2]).item() * deltakz/2
|
|
388
|
+
gz_fall_time = gz_adc.fall_time
|
|
389
|
+
dur -= gz_fall_time # Correct timing for rise and fall time of adc gradient
|
|
390
|
+
kwargs_for_gz = {"channel": 'z', "system": system, "area": gz_gradmom-gz_adc_ramp, "duration": dur}
|
|
391
|
+
try:
|
|
392
|
+
gz = make_trapezoid(**kwargs_for_gz)
|
|
393
|
+
except:
|
|
394
|
+
raise Exception('Event Time too short (gz)! Event Time: Rep: ' + str(i) + ', Event: ' +str(event))
|
|
395
|
+
|
|
396
|
+
if np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom):
|
|
397
|
+
seq.add_block(gx,gy,gz)
|
|
398
|
+
elif np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0:
|
|
399
|
+
seq.add_block(gx,gy)
|
|
400
|
+
elif np.abs(gx_gradmom) > 0 and np.abs(gz_gradmom) > 0:
|
|
401
|
+
seq.add_block(gx,gz)
|
|
402
|
+
elif np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom) > 0:
|
|
403
|
+
seq.add_block(gy,gz)
|
|
404
|
+
elif np.abs(gx_gradmom) > 0:
|
|
405
|
+
seq.add_block(gx)
|
|
406
|
+
elif np.abs(gy_gradmom) > 0:
|
|
407
|
+
seq.add_block(gy)
|
|
408
|
+
elif np.abs(gz_gradmom) > 0:
|
|
409
|
+
seq.add_block(gz)
|
|
410
|
+
else:
|
|
411
|
+
seq.add_block(make_delay(dur))
|
|
412
|
+
else: #adc mask == 1
|
|
413
|
+
if adc_start == 1:
|
|
414
|
+
pass
|
|
415
|
+
else:
|
|
416
|
+
adc_start = 1
|
|
417
|
+
if bdw_start == 0:
|
|
418
|
+
bwd = (1/rep.event_time[event])/torch.sum(torch.abs(rep.adc_usage)>0)
|
|
419
|
+
print('Bandwidth is %4d Hz/pixel' % (bwd) )
|
|
420
|
+
seq.set_definition("Bandwidth", f"{int(bwd)} Hz/px")
|
|
421
|
+
bdw_start = 1
|
|
422
|
+
|
|
423
|
+
idx_T = np.nonzero(torch.abs(rep.adc_usage))
|
|
424
|
+
dur = np.round(torch.sum(rep.event_time[idx_T],0).item(),decimals=5)
|
|
425
|
+
|
|
426
|
+
rise_time_x = 0
|
|
427
|
+
rise_time_y = 0
|
|
428
|
+
rise_time_z = 0
|
|
429
|
+
|
|
430
|
+
gx_gradmom = torch.sum(rep.gradm[idx_T,0]).item()*deltakx
|
|
431
|
+
if np.abs(gx_gradmom) > 0:
|
|
432
|
+
kwargs_for_gx = {"channel": 'x', "system": system, "flat_area": gx_gradmom, "flat_time": dur}
|
|
433
|
+
gx = make_trapezoid(**kwargs_for_gx)
|
|
434
|
+
rise_time_x = gx.rise_time
|
|
435
|
+
|
|
436
|
+
gy_gradmom = torch.sum(rep.gradm[idx_T,1]).item()*deltaky
|
|
437
|
+
if np.abs(gy_gradmom) > 0:
|
|
438
|
+
kwargs_for_gy = {"channel": 'y', "system": system, "flat_area": gy_gradmom, "flat_time": dur}
|
|
439
|
+
gy = make_trapezoid(**kwargs_for_gy)
|
|
440
|
+
rise_time_y = gy.rise_time
|
|
441
|
+
|
|
442
|
+
gz_gradmom = torch.sum(rep.gradm[idx_T,2]).item()*deltakz
|
|
443
|
+
if np.abs(gz_gradmom) > 0:
|
|
444
|
+
kwargs_for_gz = {"channel": 'z', "system": system, "flat_area": gz_gradmom, "flat_time": dur}
|
|
445
|
+
gz = make_trapezoid(**kwargs_for_gz)
|
|
446
|
+
rise_time_z = gz.rise_time
|
|
447
|
+
|
|
448
|
+
# calculate correct delay to have same starting point of flat top
|
|
449
|
+
shift = 0 # np.round(rep.event_time[idx_T[0]].item()/2,decimals=6) # heuristic delay, to be checked at scanner
|
|
450
|
+
x_delay = np.max([0,rise_time_y-rise_time_x,rise_time_z-rise_time_x])+shift
|
|
451
|
+
y_delay = np.max([0,rise_time_x-rise_time_y,rise_time_z-rise_time_y])+shift
|
|
452
|
+
z_delay = np.max([0,rise_time_x-rise_time_z,rise_time_y-rise_time_z])+shift
|
|
453
|
+
|
|
454
|
+
rise_time_x = 0
|
|
455
|
+
rise_time_y = 0
|
|
456
|
+
rise_time_z = 0
|
|
457
|
+
|
|
458
|
+
# adc gradient events are overwritten with correct delays
|
|
459
|
+
if np.abs(gx_gradmom) > 0:
|
|
460
|
+
kwargs_for_gx = {"channel": 'x', "system": system,"delay":x_delay, "flat_area": gx_gradmom, "flat_time": dur}
|
|
461
|
+
gx = make_trapezoid(**kwargs_for_gx)
|
|
462
|
+
rise_time_x = gx.rise_time
|
|
463
|
+
if np.abs(gy_gradmom) > 0:
|
|
464
|
+
kwargs_for_gy = {"channel": 'y', "system": system,"delay":y_delay, "flat_area": gy_gradmom, "flat_time": dur}
|
|
465
|
+
gy = make_trapezoid(**kwargs_for_gy)
|
|
466
|
+
rise_time_y = gy.rise_time
|
|
467
|
+
if np.abs(gz_gradmom) > 0:
|
|
468
|
+
kwargs_for_gz = {"channel": 'z', "system": system,"delay":z_delay, "flat_area": gz_gradmom, "flat_time": dur}
|
|
469
|
+
gz = make_trapezoid(**kwargs_for_gz)
|
|
470
|
+
rise_time_z = gz.rise_time
|
|
471
|
+
|
|
472
|
+
adc_delay = np.max([rise_time_x,rise_time_y,rise_time_z])+shift
|
|
473
|
+
kwargs_for_adc = {"num_samples": idx_T.size()[0], "duration": dur, "delay":(adc_delay), "phase_offset": rf_ex.phase_offset - np.pi/4}
|
|
474
|
+
adc = make_adc(**kwargs_for_adc)
|
|
475
|
+
|
|
476
|
+
# dont play zero grads (cant even do FID otherwise)
|
|
477
|
+
if rep.adc_usage[event] == -1:
|
|
478
|
+
print("Dummie ADC played out")
|
|
479
|
+
if np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom):
|
|
480
|
+
seq.add_block(gx,gy,gz)
|
|
481
|
+
elif np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0:
|
|
482
|
+
seq.add_block(gx,gy)
|
|
483
|
+
elif np.abs(gx_gradmom) > 0 and np.abs(gz_gradmom) > 0:
|
|
484
|
+
seq.add_block(gx,gz)
|
|
485
|
+
elif np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom) > 0:
|
|
486
|
+
seq.add_block(gy,gz)
|
|
487
|
+
elif np.abs(gx_gradmom) > 0:
|
|
488
|
+
seq.add_block(gx)
|
|
489
|
+
elif np.abs(gy_gradmom) > 0:
|
|
490
|
+
seq.add_block(gy)
|
|
491
|
+
elif np.abs(gz_gradmom) > 0:
|
|
492
|
+
seq.add_block(gz)
|
|
493
|
+
else:
|
|
494
|
+
if np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom):
|
|
495
|
+
seq.add_block(gx,gy,gz,adc)
|
|
496
|
+
elif np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0:
|
|
497
|
+
seq.add_block(gx,gy,adc)
|
|
498
|
+
elif np.abs(gx_gradmom) > 0 and np.abs(gz_gradmom) > 0:
|
|
499
|
+
seq.add_block(gx,gz,adc)
|
|
500
|
+
elif np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom) > 0:
|
|
501
|
+
seq.add_block(gy,gz,adc)
|
|
502
|
+
elif np.abs(gx_gradmom) > 0:
|
|
503
|
+
seq.add_block(gx,adc)
|
|
504
|
+
elif np.abs(gy_gradmom) > 0:
|
|
505
|
+
seq.add_block(gy,adc)
|
|
506
|
+
elif np.abs(gz_gradmom) > 0:
|
|
507
|
+
seq.add_block(gz,adc)
|
|
508
|
+
else:
|
|
509
|
+
seq.add_block(adc)
|
|
510
|
+
|
|
511
|
+
passes, report = seq.check_timing()
|
|
512
|
+
if not passes:
|
|
513
|
+
print("WARNING: Timing check failed:")
|
|
514
|
+
for line in report:
|
|
515
|
+
print(line, end="")
|
|
516
|
+
|
|
517
|
+
if plot_seq:
|
|
518
|
+
seq.plot()
|
|
519
|
+
seq.write(path)
|
|
520
|
+
|
|
521
|
+
append_header(path)
|
|
522
|
+
|
|
523
|
+
if write_data:
|
|
524
|
+
util.write_data_to_seq_file(seq_param, path)
|
|
525
|
+
print('Added kspace & adc_usage information to the .seq file!')
|
|
526
|
+
|
|
527
|
+
def append_header(path):
|
|
528
|
+
# append version and definitions
|
|
529
|
+
if sys.platform != 'linux':
|
|
530
|
+
try:
|
|
531
|
+
with open(r"\\141.67.249.47\MRTransfer\mrzero_src\.git\ORIG_HEAD") as file:
|
|
532
|
+
git_version = file.read()
|
|
533
|
+
except:
|
|
534
|
+
git_version = ''
|
|
535
|
+
with open(path, 'r') as fin:
|
|
536
|
+
lines = fin.read().splitlines(True)
|
|
537
|
+
|
|
538
|
+
updated_lines = []
|
|
539
|
+
updated_lines.append("# Pulseq sequence file\n")
|
|
540
|
+
updated_lines.append("# Created by MRIzero/IMR/GPI pulseq converter\n")
|
|
541
|
+
if sys.platform != 'linux':
|
|
542
|
+
updated_lines.append('# MRZero Version: 0.5, git hash: ' + git_version)
|
|
543
|
+
if sys.platform == 'linux':
|
|
544
|
+
updated_lines.append("# experiment_id: "+path.split('/')[-2]+"\n")
|
|
545
|
+
else:
|
|
546
|
+
updated_lines.append("# experiment_id: "+path.split('\\')[-2]+"\n")
|
|
547
|
+
updated_lines.append("# path: " + path + "\n")
|
|
548
|
+
updated_lines.append("\n")
|
|
549
|
+
|
|
550
|
+
updated_lines.extend(lines[3:])
|
|
551
|
+
|
|
552
|
+
with open(path, 'w') as fout:
|
|
553
|
+
fout.writelines(updated_lines)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from .pulseq_file import PulseqFile # noqa
|
|
3
|
+
from .pulse import Pulse
|
|
4
|
+
from .spoiler import Spoiler
|
|
5
|
+
from .adc import Adc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ----- PULSES -----
|
|
9
|
+
# Simulated pulses are instantaneous. Pulseq blocks containing pulse events are
|
|
10
|
+
# split in the center of the pulse shape and converted into a gradient event
|
|
11
|
+
# before the pulse and a pulse + gradient event afterwards. This should result
|
|
12
|
+
# in a simulation that has no e.g. slice selection, but where the gradeint
|
|
13
|
+
# moments seen by refocussed, excited and unaffected magnetisation are correct.
|
|
14
|
+
# ----- ADC -----
|
|
15
|
+
# As stated in the specification, all samples are placed at (n + 0.5) * raster,
|
|
16
|
+
# which means when there is a gradient and an adc sample at the same time,
|
|
17
|
+
# the adc measurement sees only the first half of the gradient. TODO: check
|
|
18
|
+
# pulseq exporters and the Siemens interpreter to see if they respect the spec
|
|
19
|
+
# or if the adc samples should rather be placed at (n + 0 or 1) * raster.
|
|
20
|
+
# ----- GRADIENTS -----
|
|
21
|
+
# Only TRAP gradients are currently supported, arbitrary gradients would
|
|
22
|
+
# require to resample the gradient onto the ADC time grid. Note that the spec
|
|
23
|
+
# does not specify what should happen in between gradient samples, but there
|
|
24
|
+
# are two sensible options:
|
|
25
|
+
# - Mimic the scanner, where interpolation is given by the electronics, but a
|
|
26
|
+
# simple linear or bezier or similar interpolation should be a good choice
|
|
27
|
+
# - Adhere to the implementation of the official pulseq exporter, which
|
|
28
|
+
# probably assumes a piecewise constant gradient
|
|
29
|
+
# NOTE: As long as we don't simulate diffusion, we don't care about the shape
|
|
30
|
+
# of a gradient if it is not measured simultaneously.
|
|
31
|
+
# ----- Additional -----
|
|
32
|
+
# Simultaneous RF and ADC events are not supported but probably don't exist in
|
|
33
|
+
# pracitce anyways.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def intermediate(
|
|
37
|
+
file: PulseqFile
|
|
38
|
+
) -> list[tuple[int, Pulse, list[Spoiler | Adc]]]:
|
|
39
|
+
seq = []
|
|
40
|
+
# Convert to intermediate representation
|
|
41
|
+
for block in file.blocks.values():
|
|
42
|
+
assert block.rf_id == 0 or block.adc_id == 0
|
|
43
|
+
|
|
44
|
+
if block.rf_id != 0:
|
|
45
|
+
seq += Pulse.parse(block, file)
|
|
46
|
+
elif block.adc_id != 0:
|
|
47
|
+
seq += Adc.parse(block, file)
|
|
48
|
+
else:
|
|
49
|
+
seq.append(Spoiler.parse(block, file))
|
|
50
|
+
|
|
51
|
+
reps = []
|
|
52
|
+
# [event_count, pulse, list of events]
|
|
53
|
+
current = [0, None, []] # Dummy for events before first pulse
|
|
54
|
+
# Split into repetitions
|
|
55
|
+
for block in seq:
|
|
56
|
+
if isinstance(block, Pulse):
|
|
57
|
+
current = [0, block, []]
|
|
58
|
+
reps.append(current)
|
|
59
|
+
else:
|
|
60
|
+
if isinstance(block, Adc):
|
|
61
|
+
current[0] += len(block.event_time)
|
|
62
|
+
else: # Spoiler
|
|
63
|
+
current[0] += 1
|
|
64
|
+
current[2].append(block)
|
|
65
|
+
|
|
66
|
+
return reps
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .pulseq_file import PulseqFile, Block
|
|
4
|
+
from .helpers import integrate
|
|
5
|
+
from .spoiler import Spoiler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Adc:
|
|
9
|
+
def __init__(self, event_time: np.ndarray, gradm: np.ndarray, phase: float
|
|
10
|
+
) -> None:
|
|
11
|
+
self.event_time = event_time
|
|
12
|
+
self.gradm = gradm
|
|
13
|
+
self.phase = phase
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def parse(cls, block: Block, pulseq: PulseqFile) -> tuple[Adc, Spoiler]:
|
|
17
|
+
adc = pulseq.adcs[block.adc_id]
|
|
18
|
+
time = np.concatenate([
|
|
19
|
+
[0.0],
|
|
20
|
+
adc.delay + np.arange(adc.num) * adc.dwell,
|
|
21
|
+
[block.duration]
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
gradm = np.zeros((adc.num + 1, 3))
|
|
25
|
+
if block.gx_id != 0:
|
|
26
|
+
grad = pulseq.grads[block.gx_id]
|
|
27
|
+
gradm[:, 0] = np.diff([integrate(grad, pulseq, t) for t in time])
|
|
28
|
+
if block.gy_id != 0:
|
|
29
|
+
grad = pulseq.grads[block.gy_id]
|
|
30
|
+
gradm[:, 1] = np.diff([integrate(grad, pulseq, t) for t in time])
|
|
31
|
+
if block.gz_id != 0:
|
|
32
|
+
grad = pulseq.grads[block.gz_id]
|
|
33
|
+
gradm[:, 2] = np.diff([integrate(grad, pulseq, t) for t in time])
|
|
34
|
+
|
|
35
|
+
event_time = np.diff(time)
|
|
36
|
+
|
|
37
|
+
fov = pulseq.definitions.fov
|
|
38
|
+
gradm[:, 0] *= fov[0]
|
|
39
|
+
gradm[:, 1] *= fov[1]
|
|
40
|
+
gradm[:, 2] *= fov[2]
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
cls(event_time[:-1], gradm[:-1, :], adc.phase),
|
|
44
|
+
Spoiler(event_time[-1], gradm[-1, :])
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return f"ADC(event_time={self.event_time}, gradm={self.gradm})"
|