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.
@@ -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)