pulseqzero 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.
- pulseqzero/__init__.py +92 -0
- pulseqzero/adapter/__init__.py +47 -0
- pulseqzero/adapter/adc.py +37 -0
- pulseqzero/adapter/delay.py +22 -0
- pulseqzero/adapter/grads.py +222 -0
- pulseqzero/adapter/opts.py +98 -0
- pulseqzero/adapter/pulses.py +253 -0
- pulseqzero/adapter/seq_convert.py +237 -0
- pulseqzero/adapter/sequence.py +66 -0
- pulseqzero-0.1.0.dist-info/LICENSE +661 -0
- pulseqzero-0.1.0.dist-info/METADATA +298 -0
- pulseqzero-0.1.0.dist-info/RECORD +14 -0
- pulseqzero-0.1.0.dist-info/WHEEL +5 -0
- pulseqzero-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from ..adapter import Opts, make_delay, make_trapezoid, calc_duration
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def make_arbitrary_rf(
|
|
6
|
+
signal,
|
|
7
|
+
flip_angle,
|
|
8
|
+
bandwidth=0,
|
|
9
|
+
delay=0,
|
|
10
|
+
dwell=None,
|
|
11
|
+
freq_offset=0,
|
|
12
|
+
no_signal_scaling=False,
|
|
13
|
+
max_grad=0,
|
|
14
|
+
max_slew=0,
|
|
15
|
+
phase_offset=0,
|
|
16
|
+
return_delay=False,
|
|
17
|
+
return_gz=False,
|
|
18
|
+
slice_thickness=0,
|
|
19
|
+
system=None,
|
|
20
|
+
time_bw_product=0,
|
|
21
|
+
use=str(),
|
|
22
|
+
):
|
|
23
|
+
if system is None:
|
|
24
|
+
system = Opts.default
|
|
25
|
+
if dwell is None:
|
|
26
|
+
dwell = system.rf_raster_time
|
|
27
|
+
delay = max(delay, system.rf_dead_time)
|
|
28
|
+
|
|
29
|
+
duration = len(signal) * dwell
|
|
30
|
+
|
|
31
|
+
rf = Pulse(
|
|
32
|
+
flip_angle,
|
|
33
|
+
duration,
|
|
34
|
+
freq_offset,
|
|
35
|
+
phase_offset,
|
|
36
|
+
delay,
|
|
37
|
+
system.rf_ringdown_time
|
|
38
|
+
)
|
|
39
|
+
ret_val = (rf, )
|
|
40
|
+
|
|
41
|
+
if return_gz:
|
|
42
|
+
if max_grad is None:
|
|
43
|
+
max_grad = system.max_grad
|
|
44
|
+
if max_slew is None:
|
|
45
|
+
max_slew = system.max_slew
|
|
46
|
+
|
|
47
|
+
if bandwidth is None:
|
|
48
|
+
bandwidth = time_bw_product / duration
|
|
49
|
+
area = bandwidth / slice_thickness * duration
|
|
50
|
+
|
|
51
|
+
gz = make_trapezoid(
|
|
52
|
+
"z", system=system,
|
|
53
|
+
flat_area=area, flat_time=duration
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if rf.delay > gz.rise_time:
|
|
57
|
+
gz.delay = rf.delay - gz.rise_time
|
|
58
|
+
if rf.delay < gz.rise_time + gz.delay:
|
|
59
|
+
rf.delay = gz.rise_time + gz.delay
|
|
60
|
+
|
|
61
|
+
ret_val = (*ret_val, gz)
|
|
62
|
+
|
|
63
|
+
if return_delay and rf.ringdown_time > 0:
|
|
64
|
+
delay = make_delay(calc_duration(rf) + rf.ringdown_time)
|
|
65
|
+
ret_val = (*ret_val, delay)
|
|
66
|
+
|
|
67
|
+
return ret_val
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def make_block_pulse(
|
|
71
|
+
flip_angle,
|
|
72
|
+
delay=0,
|
|
73
|
+
duration=None,
|
|
74
|
+
bandwidth=None,
|
|
75
|
+
time_bw_product=0.25,
|
|
76
|
+
freq_offset=0,
|
|
77
|
+
phase_offset=0,
|
|
78
|
+
return_delay=False,
|
|
79
|
+
system=None,
|
|
80
|
+
use="",
|
|
81
|
+
):
|
|
82
|
+
if system is None:
|
|
83
|
+
system = Opts.default
|
|
84
|
+
delay = max(delay, system.rf_dead_time)
|
|
85
|
+
|
|
86
|
+
if duration is None:
|
|
87
|
+
if bandwidth is None:
|
|
88
|
+
duration = 1e-4
|
|
89
|
+
else:
|
|
90
|
+
duration = time_bw_product / bandwidth
|
|
91
|
+
else:
|
|
92
|
+
assert bandwidth is None
|
|
93
|
+
|
|
94
|
+
rf = Pulse(
|
|
95
|
+
flip_angle,
|
|
96
|
+
duration,
|
|
97
|
+
freq_offset,
|
|
98
|
+
phase_offset,
|
|
99
|
+
delay,
|
|
100
|
+
system.rf_ringdown_time
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if system.rf_dead_time > rf.delay:
|
|
104
|
+
rf.delay = system.rf_dead_time
|
|
105
|
+
|
|
106
|
+
if return_delay:
|
|
107
|
+
return (rf, make_delay(calc_duration(rf) + system.rf_ringdown_time))
|
|
108
|
+
else:
|
|
109
|
+
return rf
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def make_gauss_pulse(
|
|
113
|
+
flip_angle,
|
|
114
|
+
apodization=0,
|
|
115
|
+
bandwidth=None,
|
|
116
|
+
center_pos=0.5,
|
|
117
|
+
delay=0,
|
|
118
|
+
dwell=0,
|
|
119
|
+
duration=4e-3,
|
|
120
|
+
freq_offset=0,
|
|
121
|
+
max_grad=0,
|
|
122
|
+
max_slew=0,
|
|
123
|
+
phase_offset=0,
|
|
124
|
+
return_gz=False,
|
|
125
|
+
return_delay=False,
|
|
126
|
+
slice_thickness=0,
|
|
127
|
+
system=None,
|
|
128
|
+
time_bw_product=4,
|
|
129
|
+
use=""
|
|
130
|
+
):
|
|
131
|
+
if system is None:
|
|
132
|
+
system = Opts.default
|
|
133
|
+
delay = max(delay, system.rf_dead_time)
|
|
134
|
+
|
|
135
|
+
rf = Pulse(
|
|
136
|
+
flip_angle,
|
|
137
|
+
duration,
|
|
138
|
+
freq_offset,
|
|
139
|
+
phase_offset,
|
|
140
|
+
delay,
|
|
141
|
+
system.rf_ringdown_time
|
|
142
|
+
)
|
|
143
|
+
ret_val = (rf, )
|
|
144
|
+
|
|
145
|
+
if return_gz:
|
|
146
|
+
if max_grad is None:
|
|
147
|
+
max_grad = system.max_grad
|
|
148
|
+
if max_slew is None:
|
|
149
|
+
max_slew = system.max_slew
|
|
150
|
+
|
|
151
|
+
if bandwidth is None:
|
|
152
|
+
bandwidth = time_bw_product / duration
|
|
153
|
+
area = bandwidth / slice_thickness * duration
|
|
154
|
+
|
|
155
|
+
gz = make_trapezoid(
|
|
156
|
+
"z", system=system,
|
|
157
|
+
flat_area=area, flat_time=duration
|
|
158
|
+
)
|
|
159
|
+
gzr = make_trapezoid(
|
|
160
|
+
"z", system=system,
|
|
161
|
+
area=-area * (1 - center_pos) - 0.5 * (gz.area - area)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if rf.delay > gz.rise_time:
|
|
165
|
+
gz.delay = rf.delay - gz.rise_time
|
|
166
|
+
if rf.delay < gz.rise_time + gz.delay:
|
|
167
|
+
rf.delay = gz.rise_time + gz.delay
|
|
168
|
+
|
|
169
|
+
ret_val = (*ret_val, gz, gzr)
|
|
170
|
+
|
|
171
|
+
if return_delay and rf.ringdown_time > 0:
|
|
172
|
+
delay = make_delay(calc_duration(rf) + rf.ringdown_time)
|
|
173
|
+
ret_val = (*ret_val, delay)
|
|
174
|
+
|
|
175
|
+
return ret_val
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def make_sinc_pulse(
|
|
179
|
+
flip_angle,
|
|
180
|
+
apodization=0,
|
|
181
|
+
delay=0,
|
|
182
|
+
duration=4e-3,
|
|
183
|
+
dwell=0,
|
|
184
|
+
center_pos=0.5,
|
|
185
|
+
freq_offset=0,
|
|
186
|
+
max_grad=None,
|
|
187
|
+
max_slew=None,
|
|
188
|
+
phase_offset=0,
|
|
189
|
+
return_delay=False,
|
|
190
|
+
return_gz=False,
|
|
191
|
+
slice_thickness=0,
|
|
192
|
+
system=None,
|
|
193
|
+
time_bw_product=4,
|
|
194
|
+
use=""
|
|
195
|
+
):
|
|
196
|
+
if system is None:
|
|
197
|
+
system = Opts.default
|
|
198
|
+
delay = max(delay, system.rf_dead_time)
|
|
199
|
+
|
|
200
|
+
rf = Pulse(
|
|
201
|
+
flip_angle,
|
|
202
|
+
duration,
|
|
203
|
+
freq_offset,
|
|
204
|
+
phase_offset,
|
|
205
|
+
delay,
|
|
206
|
+
system.rf_ringdown_time
|
|
207
|
+
)
|
|
208
|
+
ret_val = (rf, )
|
|
209
|
+
|
|
210
|
+
if return_gz:
|
|
211
|
+
if max_grad is None:
|
|
212
|
+
max_grad = system.max_grad
|
|
213
|
+
if max_slew is None:
|
|
214
|
+
max_slew = system.max_slew
|
|
215
|
+
|
|
216
|
+
BW = time_bw_product / duration
|
|
217
|
+
area = BW / slice_thickness * duration
|
|
218
|
+
|
|
219
|
+
gz = make_trapezoid(
|
|
220
|
+
"z", max_grad=max_grad, max_slew=max_slew,
|
|
221
|
+
flat_time=duration, flat_area=area
|
|
222
|
+
)
|
|
223
|
+
gzr = make_trapezoid(
|
|
224
|
+
"z", max_grad=max_grad, max_slew=max_slew,
|
|
225
|
+
area=-area * (1 - center_pos) - 0.5 * (gz.area - area)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if rf.delay > gz.rise_time:
|
|
229
|
+
gz.delay = rf.delay - gz.rise_time
|
|
230
|
+
if rf.delay < gz.rise_time + gz.delay:
|
|
231
|
+
rf.delay = gz.rise_time + gz.delay
|
|
232
|
+
|
|
233
|
+
ret_val = (*ret_val, gz, gzr)
|
|
234
|
+
|
|
235
|
+
if return_delay and rf.ringdown_time > 0:
|
|
236
|
+
delay = make_delay(rf.duration)
|
|
237
|
+
ret_val = (*ret_val, delay)
|
|
238
|
+
|
|
239
|
+
return ret_val
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass
|
|
243
|
+
class Pulse:
|
|
244
|
+
flip_angle: ...
|
|
245
|
+
shape_dur: ...
|
|
246
|
+
freq_offset: ... # ignored by sim
|
|
247
|
+
phase_offset: ...
|
|
248
|
+
delay: ...
|
|
249
|
+
ringdown_time: ... # important for duration
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def duration(self):
|
|
253
|
+
return self.delay + self.shape_dur + self.ringdown_time
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import MRzeroCore as mr0
|
|
3
|
+
from ..adapter import calc_duration
|
|
4
|
+
from ..adapter.pulses import Pulse
|
|
5
|
+
from ..adapter.adc import Adc
|
|
6
|
+
from ..adapter.delay import Delay
|
|
7
|
+
from ..adapter.grads import TrapGrad, FreeGrad
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def convert(pp0) -> mr0.Sequence:
|
|
11
|
+
seq = []
|
|
12
|
+
|
|
13
|
+
for block in pp0.blocks:
|
|
14
|
+
delay = None
|
|
15
|
+
adc = None
|
|
16
|
+
rf = None
|
|
17
|
+
grad_x = None
|
|
18
|
+
grad_y = None
|
|
19
|
+
grad_z = None
|
|
20
|
+
for ev in block:
|
|
21
|
+
if isinstance(ev, Delay):
|
|
22
|
+
assert delay is None
|
|
23
|
+
delay = ev
|
|
24
|
+
if isinstance(ev, Adc):
|
|
25
|
+
assert adc is None
|
|
26
|
+
adc = ev
|
|
27
|
+
if isinstance(ev, Pulse):
|
|
28
|
+
assert rf is None
|
|
29
|
+
rf = ev
|
|
30
|
+
if isinstance(ev, (TrapGrad, FreeGrad)):
|
|
31
|
+
assert ev.channel in ["x", "y", "z"]
|
|
32
|
+
if ev.channel == "x":
|
|
33
|
+
assert grad_x is None
|
|
34
|
+
grad_x = ev
|
|
35
|
+
elif ev.channel == "y":
|
|
36
|
+
assert grad_y is None
|
|
37
|
+
grad_y = ev
|
|
38
|
+
elif ev.channel == "z":
|
|
39
|
+
assert grad_z is None
|
|
40
|
+
grad_z = ev
|
|
41
|
+
|
|
42
|
+
if rf:
|
|
43
|
+
assert delay is None and adc is None
|
|
44
|
+
seq += parse_pulse(rf, grad_x, grad_y, grad_z)
|
|
45
|
+
elif adc:
|
|
46
|
+
assert delay is None
|
|
47
|
+
seq += parse_adc(adc, grad_x, grad_y, grad_z)
|
|
48
|
+
else:
|
|
49
|
+
seq += parse_spoiler(delay, grad_x, grad_y, grad_z)
|
|
50
|
+
|
|
51
|
+
reps = []
|
|
52
|
+
rep = []
|
|
53
|
+
for ev in seq:
|
|
54
|
+
if isinstance(ev, TmpPulse):
|
|
55
|
+
rep = []
|
|
56
|
+
reps.append(rep)
|
|
57
|
+
rep.append(ev)
|
|
58
|
+
|
|
59
|
+
seq = mr0.Sequence()
|
|
60
|
+
for rep_in in reps:
|
|
61
|
+
event_count = 0
|
|
62
|
+
for ev in rep_in:
|
|
63
|
+
if isinstance(ev, TmpAdc):
|
|
64
|
+
event_count += len(ev.event_time)
|
|
65
|
+
else:
|
|
66
|
+
event_count += 1
|
|
67
|
+
|
|
68
|
+
rep_out = seq.new_rep(event_count)
|
|
69
|
+
rep_out.pulse.angle = torch.as_tensor(rep_in[0].angle)
|
|
70
|
+
rep_out.pulse.phase = torch.as_tensor(rep_in[0].phase)
|
|
71
|
+
if rep_out.pulse.angle > 100 * torch.pi / 180:
|
|
72
|
+
rep_out.pulse.usage = mr0.PulseUsage.REFOC
|
|
73
|
+
else:
|
|
74
|
+
rep_out.pulse.usage = mr0.PulseUsage.EXCIT
|
|
75
|
+
|
|
76
|
+
i = 0
|
|
77
|
+
for ev in rep_in[1:]:
|
|
78
|
+
if isinstance(ev, TmpSpoiler):
|
|
79
|
+
rep_out.event_time[i] = ev.duration
|
|
80
|
+
rep_out.gradm[i, :] = ev.gradm
|
|
81
|
+
i += 1
|
|
82
|
+
else:
|
|
83
|
+
assert isinstance(ev, TmpAdc)
|
|
84
|
+
num = len(ev.event_time)
|
|
85
|
+
rep_out.event_time[i:i+num] = torch.as_tensor(ev.event_time)
|
|
86
|
+
rep_out.gradm[i:i+num, :] = torch.as_tensor(ev.gradm)
|
|
87
|
+
rep_out.adc_phase[i:i+num] = torch.pi / 2 - ev.phase
|
|
88
|
+
rep_out.adc_usage[i:i+num] = 1
|
|
89
|
+
i += num
|
|
90
|
+
|
|
91
|
+
seq.normalized_grads = False
|
|
92
|
+
return seq
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TmpPulse:
|
|
96
|
+
def __init__(self, angle, phase) -> None:
|
|
97
|
+
self.angle = angle
|
|
98
|
+
self.phase = phase
|
|
99
|
+
|
|
100
|
+
def __repr__(self) -> str:
|
|
101
|
+
from math import pi
|
|
102
|
+
return f"Pulse(angle={self.angle * 180 / pi}°, phase={self.phase * 180 / pi}°)"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TmpSpoiler:
|
|
106
|
+
def __init__(self, duration, gx, gy, gz) -> None:
|
|
107
|
+
self.duration = torch.as_tensor(duration)
|
|
108
|
+
self.gradm = torch.stack([
|
|
109
|
+
torch.as_tensor(gx),
|
|
110
|
+
torch.as_tensor(gy),
|
|
111
|
+
torch.as_tensor(gz)
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
def __repr__(self) -> str:
|
|
115
|
+
return f"Spoiler(gradm={self.gradm}, duration={self.duration})"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TmpAdc:
|
|
119
|
+
def __init__(self, event_time, gradm, phase) -> None:
|
|
120
|
+
self.event_time = event_time
|
|
121
|
+
self.gradm = gradm
|
|
122
|
+
self.phase = phase
|
|
123
|
+
|
|
124
|
+
def __repr__(self) -> str:
|
|
125
|
+
from math import pi
|
|
126
|
+
return f"Adc(phase={self.phase * 180 / pi}°, total_gradm={self.gradm.sum(0)}, total_time={self.event_time.sum(0)})"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_pulse(rf, grad_x, grad_y, grad_z) -> tuple[TmpSpoiler, TmpPulse, TmpSpoiler]:
|
|
130
|
+
t = rf.delay + rf.shape_dur / 2
|
|
131
|
+
duration = calc_duration(rf, grad_x, grad_y, grad_z)
|
|
132
|
+
|
|
133
|
+
gx1 = gx2 = gy1 = gy2 = gz1 = gz2 = 0.0
|
|
134
|
+
if grad_x:
|
|
135
|
+
gx1, gx2 = split_gradm(grad_x, t)
|
|
136
|
+
if grad_y:
|
|
137
|
+
gy1, gy2 = split_gradm(grad_y, t)
|
|
138
|
+
if grad_z:
|
|
139
|
+
gz1, gz2 = split_gradm(grad_z, t)
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
TmpSpoiler(t, gx1, gy1, gz1),
|
|
143
|
+
TmpPulse(rf.flip_angle, rf.phase_offset),
|
|
144
|
+
TmpSpoiler(duration - t, gx2, gy2, gz2)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def parse_spoiler(delay, grad_x, grad_y, grad_z) -> tuple[TmpSpoiler]:
|
|
149
|
+
duration = calc_duration(delay, grad_x, grad_y, grad_z)
|
|
150
|
+
gx = grad_x.area if grad_x is not None else 0.0
|
|
151
|
+
gy = grad_y.area if grad_y is not None else 0.0
|
|
152
|
+
gz = grad_z.area if grad_z is not None else 0.0
|
|
153
|
+
return (TmpSpoiler(duration, gx, gy, gz), )
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_adc(adc: Adc, grad_x, grad_y, grad_z) -> tuple[TmpAdc, TmpSpoiler]:
|
|
157
|
+
duration = calc_duration(adc, grad_x, grad_y, grad_z)
|
|
158
|
+
time = torch.cat([
|
|
159
|
+
torch.as_tensor(0.0).view((1, )),
|
|
160
|
+
adc.delay + torch.arange(adc.num_samples) * adc.dwell,
|
|
161
|
+
torch.as_tensor(duration).view((1, ))
|
|
162
|
+
])
|
|
163
|
+
|
|
164
|
+
gradm = torch.zeros((adc.num_samples + 2, 3))
|
|
165
|
+
if grad_x:
|
|
166
|
+
gradm[:, 0] = torch.vmap(lambda t: integrate(grad_x, t))(time)
|
|
167
|
+
if grad_y:
|
|
168
|
+
gradm[:, 1] = torch.vmap(lambda t: integrate(grad_y, t))(time)
|
|
169
|
+
if grad_z:
|
|
170
|
+
gradm[:, 2] = torch.vmap(lambda t: integrate(grad_z, t))(time)
|
|
171
|
+
|
|
172
|
+
event_time = torch.diff(time)
|
|
173
|
+
gradm = torch.diff(gradm, dim=0)
|
|
174
|
+
return (
|
|
175
|
+
TmpAdc(event_time[:-1], gradm[:-1, :], adc.phase_offset),
|
|
176
|
+
TmpSpoiler(event_time[-1], gradm[-1, 0], gradm[-1, 1], gradm[-1, 2])
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def split_gradm(grad, t):
|
|
181
|
+
before = integrate(grad, t)
|
|
182
|
+
total = integrate(grad, 1e9) # Infinity produces 0*inf = NaNs internally
|
|
183
|
+
return (before, total - before)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def integrate(grad, t):
|
|
187
|
+
if isinstance(grad, TrapGrad):
|
|
188
|
+
# heaviside could be replaced with error function for differentiability
|
|
189
|
+
def h(x):
|
|
190
|
+
return torch.heaviside(torch.as_tensor(x), torch.tensor(0.5))
|
|
191
|
+
|
|
192
|
+
# https://www.desmos.com/calculator/0q5co02ecm
|
|
193
|
+
|
|
194
|
+
d = grad.delay
|
|
195
|
+
t1 = grad.rise_time
|
|
196
|
+
t2 = grad.flat_time
|
|
197
|
+
t3 = grad.fall_time
|
|
198
|
+
T1 = d + t1
|
|
199
|
+
T12 = d + t1 + t2
|
|
200
|
+
T123 = d + t1 + t2 + t3
|
|
201
|
+
|
|
202
|
+
# Trapezoid, could be provided as derivative:
|
|
203
|
+
# f1 = h(t - d) * h(T1 - t) * (t - d) / t1
|
|
204
|
+
# f2 = h(t - T1) * h(T12 - t)
|
|
205
|
+
# f3 = h(t - T12) * h(T123 - t) * (T123 - t) / t3
|
|
206
|
+
# f = grad.amplitude * (f1 + f2 + f3)
|
|
207
|
+
|
|
208
|
+
F_inf = t1 / 2 + t2 + t3 / 2
|
|
209
|
+
F1 = h(t - d) * h(T1 - t) * 0.5 * (t - d)**2 / t1
|
|
210
|
+
F2 = h(t - T1) * h(T12 - t) * (t1 / 2 + t - T1)
|
|
211
|
+
F3 = h(t - T12) * h(T123 - t) * (F_inf - 0.5 * (T123 - t)**2 / t3)
|
|
212
|
+
F = grad.amplitude * (F1 + F2 + F3 + h(t - T123) * F_inf)
|
|
213
|
+
|
|
214
|
+
return F
|
|
215
|
+
elif isinstance(grad, FreeGrad):
|
|
216
|
+
# To stay differentiable, we don't want dynamic indexing, but instead
|
|
217
|
+
# calculate, how much of every segment of the gradient contributes
|
|
218
|
+
# https://www.desmos.com/calculator/j2vopzhb2z
|
|
219
|
+
|
|
220
|
+
# Start and end time point and amplitude of all line segments
|
|
221
|
+
t1 = torch.as_tensor(grad.times[:-1])
|
|
222
|
+
t2 = torch.as_tensor(grad.times[1:])
|
|
223
|
+
c1 = torch.as_tensor(grad.amplitudes[:-1])
|
|
224
|
+
c2 = torch.as_tensor(grad.amplitudes[1:])
|
|
225
|
+
|
|
226
|
+
# This is how much of every segment contributes, clamped to [0, width]
|
|
227
|
+
t_rel = torch.clamp(t - t1, 0 * t1, t2 - t1)
|
|
228
|
+
# The amplitude of the segment at t, will be clamped to the amplitude
|
|
229
|
+
# of the right point for segments before t and the left point for
|
|
230
|
+
# segments after; only one segment where t lies in will be interpolated
|
|
231
|
+
c_end = c1 + t_rel / (t2 - t1) * (c2 - c1)
|
|
232
|
+
# For integration, we calculate the area of the rectangle with the
|
|
233
|
+
# average height of the left and right side of the actual shape
|
|
234
|
+
c_avg = 0.5 * (c1 + c_end)
|
|
235
|
+
return (t_rel * c_avg).sum()
|
|
236
|
+
else:
|
|
237
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from copy import copy, deepcopy
|
|
2
|
+
import MRzeroCore
|
|
3
|
+
from ..adapter import calc_duration
|
|
4
|
+
from . import seq_convert
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Sequence:
|
|
8
|
+
def __init__(self, system=None, use_block_cache=True):
|
|
9
|
+
self.definitions = {}
|
|
10
|
+
self.blocks = []
|
|
11
|
+
|
|
12
|
+
def __str__(self):
|
|
13
|
+
return f"mr0 sequence adapter; ({len(self.blocks)}) blocks"
|
|
14
|
+
|
|
15
|
+
def add_block(self, *args):
|
|
16
|
+
self.blocks.append([copy(arg) for arg in args])
|
|
17
|
+
|
|
18
|
+
def check_timing(self):
|
|
19
|
+
return (True, [])
|
|
20
|
+
|
|
21
|
+
def duration(self):
|
|
22
|
+
duration = sum(calc_duration(*block) for block in self.blocks)
|
|
23
|
+
num_blocks = len(self.blocks)
|
|
24
|
+
event_count = len(filter(lambda b: len(b) > 0, self.blocks))
|
|
25
|
+
return duration, num_blocks, event_count
|
|
26
|
+
|
|
27
|
+
def get_definition(self, key):
|
|
28
|
+
if key in self.definitions:
|
|
29
|
+
return self.definitions[key]
|
|
30
|
+
else:
|
|
31
|
+
return ""
|
|
32
|
+
|
|
33
|
+
def plot(
|
|
34
|
+
self,
|
|
35
|
+
label=str(),
|
|
36
|
+
show_blocks=False,
|
|
37
|
+
save=False,
|
|
38
|
+
time_range=(0, float("inf")),
|
|
39
|
+
time_disp="s",
|
|
40
|
+
grad_disp="kHz/m",
|
|
41
|
+
plot_now=True
|
|
42
|
+
):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def remove_duplicates(self, in_place=False):
|
|
46
|
+
if in_place:
|
|
47
|
+
return self
|
|
48
|
+
else:
|
|
49
|
+
return deepcopy(self)
|
|
50
|
+
|
|
51
|
+
def set_definition(self, key, value):
|
|
52
|
+
self.definitions[key] = value
|
|
53
|
+
|
|
54
|
+
def test_report(self):
|
|
55
|
+
return "No report generated in mr0 mode"
|
|
56
|
+
|
|
57
|
+
def write(self, name, create_signature, remove_duplicates):
|
|
58
|
+
if create_signature:
|
|
59
|
+
return ""
|
|
60
|
+
else:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
# What we do all of this for:
|
|
64
|
+
# To intercept pulseq calls and build an MR-zero sequence from it
|
|
65
|
+
def to_mr0(self) -> MRzeroCore.Sequence:
|
|
66
|
+
return seq_convert.convert(self)
|