pulseqzero 0.1.2__tar.gz → 0.1.4__tar.gz
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-0.1.2/src/pulseqzero.egg-info → pulseqzero-0.1.4}/PKG-INFO +46 -30
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/README.md +45 -29
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/pyproject.toml +1 -1
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/__init__.py +7 -1
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/adapter/__init__.py +1 -1
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/adapter/grads.py +23 -11
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/adapter/pulses.py +3 -3
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/adapter/seq_convert.py +17 -14
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/adapter/sequence.py +1 -1
- pulseqzero-0.1.4/src/pulseqzero/math.py +71 -0
- {pulseqzero-0.1.2 → pulseqzero-0.1.4/src/pulseqzero.egg-info}/PKG-INFO +46 -30
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero.egg-info/SOURCES.txt +1 -0
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/LICENSE +0 -0
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/setup.cfg +0 -0
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/adapter/adc.py +0 -0
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/adapter/delay.py +0 -0
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero/adapter/opts.py +0 -0
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero.egg-info/dependency_links.txt +0 -0
- {pulseqzero-0.1.2 → pulseqzero-0.1.4}/src/pulseqzero.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pulseqzero
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Use PyPulseq for MRI sequence definitions in MR-zero
|
|
5
5
|
Author-email: Jonathan Endres <jonathan.endres@uk-erlangen.de>
|
|
6
6
|
Project-URL: Repository, https://github.com/pulseq-frame/pulseq-zero
|
|
@@ -54,7 +54,7 @@ It relies on the following amazing projects:
|
|
|
54
54
|
|
|
55
55
|
## 2. Usage
|
|
56
56
|
|
|
57
|
-
Example scripts are provided in [
|
|
57
|
+
Example scripts are provided in the [tests](tests) folder.
|
|
58
58
|
They are modified versions of the PyPulseq 1.4.2 examples found [here](https://github.com/imr-framework/pypulseq/tree/v1.4.2/pypulseq/seq_examples/scripts).
|
|
59
59
|
The changes that are typically necessary to convert from a pypulseq sequence script to Pulseq-zero are as follows:
|
|
60
60
|
|
|
@@ -166,6 +166,49 @@ If you want to contribute to Pulseq-zero or make local changes to it, the easies
|
|
|
166
166
|
|
|
167
167
|
## 4. API
|
|
168
168
|
|
|
169
|
+
### Additional API
|
|
170
|
+
|
|
171
|
+
Pulseq requires many events to align with the block-, gradient- or ADC raster.
|
|
172
|
+
For this, rounding is necessary, which is not differentiable.
|
|
173
|
+
As a workaround, Pulseq-zero includes differentiable rounding functions, which are wrappers around the corresponding PyTorch functions, but which behave like the identity function for backpropagation.
|
|
174
|
+
This means they are invisible to the calculation of gradients, which is okay as long as the rounding is small compared to the changes to the optimized parameters, which is typically the case with small raster times.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
my_param = torch.tensor(1.5, requires_grad=True)
|
|
178
|
+
some_calc = pp.round(torch.sin(my_param))
|
|
179
|
+
some_calc.backward()
|
|
180
|
+
|
|
181
|
+
assert some_calc == 1
|
|
182
|
+
assert my_param.grad == torch.cos(my_param)
|
|
183
|
+
```
|
|
184
|
+
Use pulseq-zeros rounding functions instead of e.g. those of numpy or torch if it is applied to timings (or other sequence properties) that are part of an optimization.
|
|
185
|
+
|
|
186
|
+
In the `mr0_mode` context, all PyPulseq functions are swapped for Pulseq-zero implementations that track all calls so that the sequence can be converted to MR-zero later:
|
|
187
|
+
```python
|
|
188
|
+
with pulseqzero.mr0_mode():
|
|
189
|
+
seq = build_my_seq()
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If you use some functions that are only available in PyPulseq but not Pulseq-zero (PNS computations or similar), you can check if `mr0_mode` is activated:
|
|
193
|
+
```python
|
|
194
|
+
if pulseqzero.is_mr0_mode():
|
|
195
|
+
pass
|
|
196
|
+
else:
|
|
197
|
+
seq.calculate_pns()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Finally, the sequence object returned in `mr0_mode` has one method that is not available otherwise and forms the central point of Pulseq-zero:
|
|
201
|
+
```python
|
|
202
|
+
with pulseqzero.mr0_mode():
|
|
203
|
+
seq = build_my_seq()
|
|
204
|
+
# This function here
|
|
205
|
+
mr0_seq = seq.to_mr0()
|
|
206
|
+
# Now do some simulations with mr0_seq!
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
### PyPulseq API
|
|
211
|
+
|
|
169
212
|
The following is a list of all functions and methods exposed in PyPulseq 1.4.2 when importing it directly.
|
|
170
213
|
Pulseq-zero tries to provide all of those - it is currently not planned to cover other functions that are available internally in PyPulseq but not exposed this way.
|
|
171
214
|
If you need one of them, file an issue or submit a pull request.
|
|
@@ -270,7 +313,7 @@ The reason for disabling is either that a differentiable re-implementation if ou
|
|
|
270
313
|
| `make_label` | `None` |
|
|
271
314
|
| `make_extended_trapezoid_area` | **error** |
|
|
272
315
|
| `calc_rf_bandwidth` | returns zeroes as mr0 mode doesn't store pulse shapes |
|
|
273
|
-
| `calc_rf_center` | returns
|
|
316
|
+
| `calc_rf_center` | returns (shape_dur / 2, 0) - mr0 mode ignores (assymetric) pulse shapes |
|
|
274
317
|
| `points_to_waveform` | **error** |
|
|
275
318
|
|
|
276
319
|
### Altered behaviour
|
|
@@ -296,33 +339,6 @@ Pulses have no shape, `center_pos` will influence the returned `gzr` but nothing
|
|
|
296
339
|
| `make_extended_trapezoid` | skipping some checks and ignoring `convert_to_arbitrary`; `area` is a computed property, no `first` or `last` |
|
|
297
340
|
| `Sequence` | way less internal bookkeeping, most variables are missing, reports etc. are not calculated |
|
|
298
341
|
|
|
299
|
-
### Additional API
|
|
300
|
-
|
|
301
|
-
This API is where Pulseq-zero comes into play:
|
|
302
|
-
|
|
303
|
-
In the `mr0_mode` context, all PyPulseq functions are swapped for Pulseq-zero implementations that track all calls so that the sequence can be converted to MR-zero later:
|
|
304
|
-
```python
|
|
305
|
-
with pulseqzero.mr0_mode():
|
|
306
|
-
seq = build_my_seq()
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
If you use some functions that are only available in PyPulseq but not Pulseq-zero (PNS computations or similar), you can check if `mr0_mode` is activated:
|
|
310
|
-
```python
|
|
311
|
-
if pulseqzero.is_mr0_mode():
|
|
312
|
-
pass
|
|
313
|
-
else:
|
|
314
|
-
seq.calculate_pns()
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
Finally, the sequence object returned in `mr0_mode` has one method that is not available otherwise and forms the central point of Pulseq-zero:
|
|
318
|
-
```python
|
|
319
|
-
with pulseqzero.mr0_mode():
|
|
320
|
-
seq = build_my_seq()
|
|
321
|
-
# This function here
|
|
322
|
-
mr0_seq = seq.to_mr0()
|
|
323
|
-
# Now do some simulations with mr0_seq!
|
|
324
|
-
```
|
|
325
|
-
|
|
326
342
|
|
|
327
343
|
## 5. References
|
|
328
344
|
|
|
@@ -42,7 +42,7 @@ It relies on the following amazing projects:
|
|
|
42
42
|
|
|
43
43
|
## 2. Usage
|
|
44
44
|
|
|
45
|
-
Example scripts are provided in [
|
|
45
|
+
Example scripts are provided in the [tests](tests) folder.
|
|
46
46
|
They are modified versions of the PyPulseq 1.4.2 examples found [here](https://github.com/imr-framework/pypulseq/tree/v1.4.2/pypulseq/seq_examples/scripts).
|
|
47
47
|
The changes that are typically necessary to convert from a pypulseq sequence script to Pulseq-zero are as follows:
|
|
48
48
|
|
|
@@ -154,6 +154,49 @@ If you want to contribute to Pulseq-zero or make local changes to it, the easies
|
|
|
154
154
|
|
|
155
155
|
## 4. API
|
|
156
156
|
|
|
157
|
+
### Additional API
|
|
158
|
+
|
|
159
|
+
Pulseq requires many events to align with the block-, gradient- or ADC raster.
|
|
160
|
+
For this, rounding is necessary, which is not differentiable.
|
|
161
|
+
As a workaround, Pulseq-zero includes differentiable rounding functions, which are wrappers around the corresponding PyTorch functions, but which behave like the identity function for backpropagation.
|
|
162
|
+
This means they are invisible to the calculation of gradients, which is okay as long as the rounding is small compared to the changes to the optimized parameters, which is typically the case with small raster times.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
my_param = torch.tensor(1.5, requires_grad=True)
|
|
166
|
+
some_calc = pp.round(torch.sin(my_param))
|
|
167
|
+
some_calc.backward()
|
|
168
|
+
|
|
169
|
+
assert some_calc == 1
|
|
170
|
+
assert my_param.grad == torch.cos(my_param)
|
|
171
|
+
```
|
|
172
|
+
Use pulseq-zeros rounding functions instead of e.g. those of numpy or torch if it is applied to timings (or other sequence properties) that are part of an optimization.
|
|
173
|
+
|
|
174
|
+
In the `mr0_mode` context, all PyPulseq functions are swapped for Pulseq-zero implementations that track all calls so that the sequence can be converted to MR-zero later:
|
|
175
|
+
```python
|
|
176
|
+
with pulseqzero.mr0_mode():
|
|
177
|
+
seq = build_my_seq()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If you use some functions that are only available in PyPulseq but not Pulseq-zero (PNS computations or similar), you can check if `mr0_mode` is activated:
|
|
181
|
+
```python
|
|
182
|
+
if pulseqzero.is_mr0_mode():
|
|
183
|
+
pass
|
|
184
|
+
else:
|
|
185
|
+
seq.calculate_pns()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Finally, the sequence object returned in `mr0_mode` has one method that is not available otherwise and forms the central point of Pulseq-zero:
|
|
189
|
+
```python
|
|
190
|
+
with pulseqzero.mr0_mode():
|
|
191
|
+
seq = build_my_seq()
|
|
192
|
+
# This function here
|
|
193
|
+
mr0_seq = seq.to_mr0()
|
|
194
|
+
# Now do some simulations with mr0_seq!
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
### PyPulseq API
|
|
199
|
+
|
|
157
200
|
The following is a list of all functions and methods exposed in PyPulseq 1.4.2 when importing it directly.
|
|
158
201
|
Pulseq-zero tries to provide all of those - it is currently not planned to cover other functions that are available internally in PyPulseq but not exposed this way.
|
|
159
202
|
If you need one of them, file an issue or submit a pull request.
|
|
@@ -258,7 +301,7 @@ The reason for disabling is either that a differentiable re-implementation if ou
|
|
|
258
301
|
| `make_label` | `None` |
|
|
259
302
|
| `make_extended_trapezoid_area` | **error** |
|
|
260
303
|
| `calc_rf_bandwidth` | returns zeroes as mr0 mode doesn't store pulse shapes |
|
|
261
|
-
| `calc_rf_center` | returns
|
|
304
|
+
| `calc_rf_center` | returns (shape_dur / 2, 0) - mr0 mode ignores (assymetric) pulse shapes |
|
|
262
305
|
| `points_to_waveform` | **error** |
|
|
263
306
|
|
|
264
307
|
### Altered behaviour
|
|
@@ -284,33 +327,6 @@ Pulses have no shape, `center_pos` will influence the returned `gzr` but nothing
|
|
|
284
327
|
| `make_extended_trapezoid` | skipping some checks and ignoring `convert_to_arbitrary`; `area` is a computed property, no `first` or `last` |
|
|
285
328
|
| `Sequence` | way less internal bookkeeping, most variables are missing, reports etc. are not calculated |
|
|
286
329
|
|
|
287
|
-
### Additional API
|
|
288
|
-
|
|
289
|
-
This API is where Pulseq-zero comes into play:
|
|
290
|
-
|
|
291
|
-
In the `mr0_mode` context, all PyPulseq functions are swapped for Pulseq-zero implementations that track all calls so that the sequence can be converted to MR-zero later:
|
|
292
|
-
```python
|
|
293
|
-
with pulseqzero.mr0_mode():
|
|
294
|
-
seq = build_my_seq()
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
If you use some functions that are only available in PyPulseq but not Pulseq-zero (PNS computations or similar), you can check if `mr0_mode` is activated:
|
|
298
|
-
```python
|
|
299
|
-
if pulseqzero.is_mr0_mode():
|
|
300
|
-
pass
|
|
301
|
-
else:
|
|
302
|
-
seq.calculate_pns()
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
Finally, the sequence object returned in `mr0_mode` has one method that is not available otherwise and forms the central point of Pulseq-zero:
|
|
306
|
-
```python
|
|
307
|
-
with pulseqzero.mr0_mode():
|
|
308
|
-
seq = build_my_seq()
|
|
309
|
-
# This function here
|
|
310
|
-
mr0_seq = seq.to_mr0()
|
|
311
|
-
# Now do some simulations with mr0_seq!
|
|
312
|
-
```
|
|
313
|
-
|
|
314
330
|
|
|
315
331
|
## 5. References
|
|
316
332
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
2
|
|
|
3
|
+
from .math import ceil, floor, round
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class Impl:
|
|
5
7
|
def __init__(self):
|
|
@@ -26,7 +28,11 @@ class Impl:
|
|
|
26
28
|
self.calc_rf_center = pp.calc_rf_center
|
|
27
29
|
self.make_adc = pp.make_adc
|
|
28
30
|
self.make_adiabatic_pulse = pp.make_adiabatic_pulse
|
|
29
|
-
|
|
31
|
+
try: # Fix for pypulseq 1.4.2 which doesn't re-export this
|
|
32
|
+
self.make_arbitrary_grad = pp.make_arbitrary_grad
|
|
33
|
+
except ImportError:
|
|
34
|
+
from pypulseq.make_arbitrary_grad import make_arbitrary_grad
|
|
35
|
+
self.make_arbitrary_grad = make_arbitrary_grad
|
|
30
36
|
self.make_arbitrary_rf = pp.make_arbitrary_rf
|
|
31
37
|
self.make_block_pulse = pp.make_block_pulse
|
|
32
38
|
self.sigpy_n_seq = pp.sigpy_n_seq
|
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from copy import copy
|
|
3
3
|
import numpy as np
|
|
4
4
|
from ..adapter import Opts, calc_duration
|
|
5
|
+
from ..math import ceil
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def scale_grad(grad, scale):
|
|
@@ -22,17 +23,17 @@ def split_gradient(grad, system):
|
|
|
22
23
|
ramp_up = make_extended_trapezoid(
|
|
23
24
|
channel=grad.channel,
|
|
24
25
|
amplitudes=np.array([0, grad.amplitude]),
|
|
25
|
-
times=np.
|
|
26
|
+
times=np.array([0, grad.rise_time])
|
|
26
27
|
)
|
|
27
28
|
flat_top = make_extended_trapezoid(
|
|
28
29
|
channel=grad.channel,
|
|
29
30
|
amplitudes=np.array([grad.amplitude, grad.amplitude]),
|
|
30
|
-
times=np.
|
|
31
|
+
times=np.array([grad.rise_time, grad.rise_time + grad.flat_time])
|
|
31
32
|
)
|
|
32
33
|
ramp_down = make_extended_trapezoid(
|
|
33
34
|
channel=grad.channel,
|
|
34
35
|
amplitudes=np.array([grad.amplitude, 0]),
|
|
35
|
-
times=np.
|
|
36
|
+
times=np.array([grad.rise_time + grad.flat_time, total_duration])
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
return ramp_up, flat_top, ramp_down
|
|
@@ -77,7 +78,9 @@ def make_trapezoid(
|
|
|
77
78
|
new_amp = flat_area / flat_time
|
|
78
79
|
|
|
79
80
|
if rise_time is None:
|
|
80
|
-
rise_time = abs(new_amp) / max_slew
|
|
81
|
+
rise_time = ceil(abs(new_amp) / max_slew / system.grad_raster_time) * system.grad_raster_time
|
|
82
|
+
if rise_time == 0:
|
|
83
|
+
rise_time = system.grad_raster_time
|
|
81
84
|
if fall_time is None:
|
|
82
85
|
fall_time = rise_time
|
|
83
86
|
|
|
@@ -86,7 +89,9 @@ def make_trapezoid(
|
|
|
86
89
|
assert area is not None
|
|
87
90
|
|
|
88
91
|
if rise_time is None:
|
|
89
|
-
_, rise_time, flat_time, fall_time = calc_params_for_area(
|
|
92
|
+
_, rise_time, flat_time, fall_time = calc_params_for_area(
|
|
93
|
+
area, max_slew, max_grad, system.grad_raster_time
|
|
94
|
+
)
|
|
90
95
|
assert duration >= rise_time + flat_time + fall_time
|
|
91
96
|
|
|
92
97
|
dC = 1 / abs(2 * max_slew)
|
|
@@ -101,7 +106,9 @@ def make_trapezoid(
|
|
|
101
106
|
new_amp = amplitude
|
|
102
107
|
|
|
103
108
|
if rise_time is None:
|
|
104
|
-
rise_time = abs(new_amp) / max_slew
|
|
109
|
+
rise_time = ceil(abs(new_amp) / max_slew / system.grad_raster_time) * system.grad_raster_time
|
|
110
|
+
if rise_time == 0:
|
|
111
|
+
rise_time = system.grad_raster_time
|
|
105
112
|
if fall_time is None:
|
|
106
113
|
fall_time = rise_time
|
|
107
114
|
flat_time = duration - rise_time - fall_time
|
|
@@ -111,7 +118,9 @@ def make_trapezoid(
|
|
|
111
118
|
|
|
112
119
|
else:
|
|
113
120
|
assert area is not None
|
|
114
|
-
new_amp, rise_time, flat_time, fall_time = calc_params_for_area(
|
|
121
|
+
new_amp, rise_time, flat_time, fall_time = calc_params_for_area(
|
|
122
|
+
area, max_slew, max_grad, system.grad_raster_time
|
|
123
|
+
)
|
|
115
124
|
|
|
116
125
|
return TrapGrad(
|
|
117
126
|
channel,
|
|
@@ -123,15 +132,18 @@ def make_trapezoid(
|
|
|
123
132
|
)
|
|
124
133
|
|
|
125
134
|
|
|
126
|
-
def calc_params_for_area(area, max_slew, max_grad):
|
|
127
|
-
rise_time = (abs(area) / max_slew)**0.5
|
|
135
|
+
def calc_params_for_area(area, max_slew, max_grad, grad_raster_time):
|
|
136
|
+
rise_time = ceil((abs(area) / max_slew)**0.5 / grad_raster_time) * grad_raster_time
|
|
128
137
|
amplitude = area / rise_time
|
|
129
138
|
t_eff = rise_time
|
|
130
139
|
|
|
131
140
|
if abs(amplitude) > max_grad:
|
|
132
|
-
t_eff = abs(area) / max_grad
|
|
141
|
+
t_eff = ceil(abs(area) / max_grad / grad_raster_time) * grad_raster_time
|
|
133
142
|
amplitude = area / t_eff
|
|
134
|
-
rise_time = abs(amplitude) / max_slew
|
|
143
|
+
rise_time = ceil(abs(amplitude) / max_slew / grad_raster_time) * grad_raster_time
|
|
144
|
+
|
|
145
|
+
if rise_time == 0:
|
|
146
|
+
rise_time = grad_raster_time
|
|
135
147
|
|
|
136
148
|
flat_time = t_eff - rise_time
|
|
137
149
|
fall_time = rise_time
|
|
@@ -225,11 +225,11 @@ def make_sinc_pulse(
|
|
|
225
225
|
area = BW / slice_thickness * duration
|
|
226
226
|
|
|
227
227
|
gz = make_trapezoid(
|
|
228
|
-
"z",
|
|
229
|
-
|
|
228
|
+
"z", system=system,
|
|
229
|
+
flat_area=area, flat_time=duration
|
|
230
230
|
)
|
|
231
231
|
gzr = make_trapezoid(
|
|
232
|
-
"z",
|
|
232
|
+
"z", system=system,
|
|
233
233
|
area=-area * (1 - center_pos) - 0.5 * (gz.area - area)
|
|
234
234
|
)
|
|
235
235
|
|
|
@@ -40,11 +40,10 @@ def convert(pp0) -> mr0.Sequence:
|
|
|
40
40
|
grad_z = ev
|
|
41
41
|
|
|
42
42
|
if rf:
|
|
43
|
-
assert
|
|
44
|
-
seq += parse_pulse(rf, grad_x, grad_y, grad_z)
|
|
43
|
+
assert adc is None
|
|
44
|
+
seq += parse_pulse(delay, rf, grad_x, grad_y, grad_z)
|
|
45
45
|
elif adc:
|
|
46
|
-
|
|
47
|
-
seq += parse_adc(adc, grad_x, grad_y, grad_z)
|
|
46
|
+
seq += parse_adc(delay, adc, grad_x, grad_y, grad_z)
|
|
48
47
|
else:
|
|
49
48
|
seq += parse_spoiler(delay, grad_x, grad_y, grad_z)
|
|
50
49
|
|
|
@@ -129,9 +128,9 @@ class TmpAdc:
|
|
|
129
128
|
return f"Adc(phase={self.phase * 180 / pi}°, total_gradm={self.gradm.sum(0)}, total_time={self.event_time.sum(0)})"
|
|
130
129
|
|
|
131
130
|
|
|
132
|
-
def parse_pulse(rf, grad_x, grad_y, grad_z) -> tuple[TmpSpoiler, TmpPulse, TmpSpoiler]:
|
|
131
|
+
def parse_pulse(delay, rf, grad_x, grad_y, grad_z) -> tuple[TmpSpoiler, TmpPulse, TmpSpoiler]:
|
|
133
132
|
t = rf.delay + rf.shape_dur / 2
|
|
134
|
-
duration = calc_duration(rf, grad_x, grad_y, grad_z)
|
|
133
|
+
duration = calc_duration(delay, rf, grad_x, grad_y, grad_z)
|
|
135
134
|
|
|
136
135
|
gx1 = gx2 = gy1 = gy2 = gz1 = gz2 = 0.0
|
|
137
136
|
if grad_x:
|
|
@@ -156,11 +155,12 @@ def parse_spoiler(delay, grad_x, grad_y, grad_z) -> tuple[TmpSpoiler]:
|
|
|
156
155
|
return (TmpSpoiler(duration, gx, gy, gz), )
|
|
157
156
|
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
# TODO: why does only adc have typing?
|
|
159
|
+
def parse_adc(delay, adc: Adc, grad_x, grad_y, grad_z) -> tuple[TmpAdc, TmpSpoiler]:
|
|
160
|
+
duration = calc_duration(delay, adc, grad_x, grad_y, grad_z)
|
|
161
161
|
time = torch.cat([
|
|
162
162
|
torch.as_tensor(0.0).view((1, )),
|
|
163
|
-
adc.delay + torch.arange(adc.num_samples) * adc.dwell,
|
|
163
|
+
adc.delay + (torch.arange(adc.num_samples) + 0.5) * adc.dwell,
|
|
164
164
|
torch.as_tensor(duration).view((1, ))
|
|
165
165
|
])
|
|
166
166
|
|
|
@@ -190,7 +190,10 @@ def integrate(grad, t):
|
|
|
190
190
|
if isinstance(grad, TrapGrad):
|
|
191
191
|
# heaviside could be replaced with error function for differentiability
|
|
192
192
|
def h(x):
|
|
193
|
-
|
|
193
|
+
try:
|
|
194
|
+
return torch.heaviside(torch.as_tensor(x), torch.tensor(0.5, dtype=x.dtype))
|
|
195
|
+
except AttributeError:
|
|
196
|
+
return 0 if x < 0 else 1 if x > 0 else 0.5
|
|
194
197
|
|
|
195
198
|
# https://www.desmos.com/calculator/0q5co02ecm
|
|
196
199
|
|
|
@@ -221,10 +224,10 @@ def integrate(grad, t):
|
|
|
221
224
|
# https://www.desmos.com/calculator/j2vopzhb2z
|
|
222
225
|
|
|
223
226
|
# Start and end time point and amplitude of all line segments
|
|
224
|
-
t1 = torch.as_tensor(grad.
|
|
225
|
-
t2 = torch.as_tensor(grad.
|
|
226
|
-
c1 = torch.as_tensor(grad.
|
|
227
|
-
c2 = torch.as_tensor(grad.
|
|
227
|
+
t1 = torch.as_tensor(grad.tt[:-1])
|
|
228
|
+
t2 = torch.as_tensor(grad.tt[1:])
|
|
229
|
+
c1 = torch.as_tensor(grad.waveform[:-1])
|
|
230
|
+
c2 = torch.as_tensor(grad.waveform[1:])
|
|
228
231
|
|
|
229
232
|
# This is how much of every segment contributes, clamped to [0, width]
|
|
230
233
|
t_rel = torch.clamp(t - t1, 0 * t1, t2 - t1)
|
|
@@ -21,7 +21,7 @@ class Sequence:
|
|
|
21
21
|
def duration(self):
|
|
22
22
|
duration = sum(calc_duration(*block) for block in self.blocks)
|
|
23
23
|
num_blocks = len(self.blocks)
|
|
24
|
-
event_count =
|
|
24
|
+
event_count = sum(len(b) for b in self.blocks)
|
|
25
25
|
return duration, num_blocks, event_count
|
|
26
26
|
|
|
27
27
|
def get_definition(self, key):
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Ceil(torch.autograd.Function):
|
|
6
|
+
@staticmethod
|
|
7
|
+
def forward(x):
|
|
8
|
+
return torch.ceil(x)
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def setup_context(ctx, inputs, output):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def backward(ctx, grad_output):
|
|
16
|
+
return grad_output
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Floor(torch.autograd.Function):
|
|
20
|
+
@staticmethod
|
|
21
|
+
def forward(x):
|
|
22
|
+
return torch.floor(x)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def setup_context(ctx, inputs, output):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def backward(ctx, grad_output):
|
|
30
|
+
return grad_output
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Round(torch.autograd.Function):
|
|
34
|
+
@staticmethod
|
|
35
|
+
def forward(x):
|
|
36
|
+
return torch.round(x)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def setup_context(ctx, inputs, output):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def backward(ctx, grad_output):
|
|
44
|
+
return grad_output
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def ceil(x: torch.Tensor | float) -> torch.Tensor:
|
|
48
|
+
"""Differentiable version of torch.ceil.
|
|
49
|
+
For gradient calculation, this mimicks the identity function."""
|
|
50
|
+
try:
|
|
51
|
+
return Ceil.apply(x)
|
|
52
|
+
except TypeError:
|
|
53
|
+
return torch.as_tensor(np.ceil(x))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def floor(x: torch.Tensor) -> torch.Tensor:
|
|
57
|
+
"""Differentiable version of torch.floor.
|
|
58
|
+
For gradient calculation, this mimicks the identity function."""
|
|
59
|
+
try:
|
|
60
|
+
return Floor.apply(x)
|
|
61
|
+
except TypeError:
|
|
62
|
+
return torch.as_tensor(np.floor(x))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def round(x: torch.Tensor) -> torch.Tensor:
|
|
66
|
+
"""Differentiable version of torch.round.
|
|
67
|
+
For gradient calculation, this mimicks the identity function."""
|
|
68
|
+
try:
|
|
69
|
+
return Round.apply(x)
|
|
70
|
+
except TypeError:
|
|
71
|
+
return torch.as_tensor(np.round(x))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pulseqzero
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Use PyPulseq for MRI sequence definitions in MR-zero
|
|
5
5
|
Author-email: Jonathan Endres <jonathan.endres@uk-erlangen.de>
|
|
6
6
|
Project-URL: Repository, https://github.com/pulseq-frame/pulseq-zero
|
|
@@ -54,7 +54,7 @@ It relies on the following amazing projects:
|
|
|
54
54
|
|
|
55
55
|
## 2. Usage
|
|
56
56
|
|
|
57
|
-
Example scripts are provided in [
|
|
57
|
+
Example scripts are provided in the [tests](tests) folder.
|
|
58
58
|
They are modified versions of the PyPulseq 1.4.2 examples found [here](https://github.com/imr-framework/pypulseq/tree/v1.4.2/pypulseq/seq_examples/scripts).
|
|
59
59
|
The changes that are typically necessary to convert from a pypulseq sequence script to Pulseq-zero are as follows:
|
|
60
60
|
|
|
@@ -166,6 +166,49 @@ If you want to contribute to Pulseq-zero or make local changes to it, the easies
|
|
|
166
166
|
|
|
167
167
|
## 4. API
|
|
168
168
|
|
|
169
|
+
### Additional API
|
|
170
|
+
|
|
171
|
+
Pulseq requires many events to align with the block-, gradient- or ADC raster.
|
|
172
|
+
For this, rounding is necessary, which is not differentiable.
|
|
173
|
+
As a workaround, Pulseq-zero includes differentiable rounding functions, which are wrappers around the corresponding PyTorch functions, but which behave like the identity function for backpropagation.
|
|
174
|
+
This means they are invisible to the calculation of gradients, which is okay as long as the rounding is small compared to the changes to the optimized parameters, which is typically the case with small raster times.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
my_param = torch.tensor(1.5, requires_grad=True)
|
|
178
|
+
some_calc = pp.round(torch.sin(my_param))
|
|
179
|
+
some_calc.backward()
|
|
180
|
+
|
|
181
|
+
assert some_calc == 1
|
|
182
|
+
assert my_param.grad == torch.cos(my_param)
|
|
183
|
+
```
|
|
184
|
+
Use pulseq-zeros rounding functions instead of e.g. those of numpy or torch if it is applied to timings (or other sequence properties) that are part of an optimization.
|
|
185
|
+
|
|
186
|
+
In the `mr0_mode` context, all PyPulseq functions are swapped for Pulseq-zero implementations that track all calls so that the sequence can be converted to MR-zero later:
|
|
187
|
+
```python
|
|
188
|
+
with pulseqzero.mr0_mode():
|
|
189
|
+
seq = build_my_seq()
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If you use some functions that are only available in PyPulseq but not Pulseq-zero (PNS computations or similar), you can check if `mr0_mode` is activated:
|
|
193
|
+
```python
|
|
194
|
+
if pulseqzero.is_mr0_mode():
|
|
195
|
+
pass
|
|
196
|
+
else:
|
|
197
|
+
seq.calculate_pns()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Finally, the sequence object returned in `mr0_mode` has one method that is not available otherwise and forms the central point of Pulseq-zero:
|
|
201
|
+
```python
|
|
202
|
+
with pulseqzero.mr0_mode():
|
|
203
|
+
seq = build_my_seq()
|
|
204
|
+
# This function here
|
|
205
|
+
mr0_seq = seq.to_mr0()
|
|
206
|
+
# Now do some simulations with mr0_seq!
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
### PyPulseq API
|
|
211
|
+
|
|
169
212
|
The following is a list of all functions and methods exposed in PyPulseq 1.4.2 when importing it directly.
|
|
170
213
|
Pulseq-zero tries to provide all of those - it is currently not planned to cover other functions that are available internally in PyPulseq but not exposed this way.
|
|
171
214
|
If you need one of them, file an issue or submit a pull request.
|
|
@@ -270,7 +313,7 @@ The reason for disabling is either that a differentiable re-implementation if ou
|
|
|
270
313
|
| `make_label` | `None` |
|
|
271
314
|
| `make_extended_trapezoid_area` | **error** |
|
|
272
315
|
| `calc_rf_bandwidth` | returns zeroes as mr0 mode doesn't store pulse shapes |
|
|
273
|
-
| `calc_rf_center` | returns
|
|
316
|
+
| `calc_rf_center` | returns (shape_dur / 2, 0) - mr0 mode ignores (assymetric) pulse shapes |
|
|
274
317
|
| `points_to_waveform` | **error** |
|
|
275
318
|
|
|
276
319
|
### Altered behaviour
|
|
@@ -296,33 +339,6 @@ Pulses have no shape, `center_pos` will influence the returned `gzr` but nothing
|
|
|
296
339
|
| `make_extended_trapezoid` | skipping some checks and ignoring `convert_to_arbitrary`; `area` is a computed property, no `first` or `last` |
|
|
297
340
|
| `Sequence` | way less internal bookkeeping, most variables are missing, reports etc. are not calculated |
|
|
298
341
|
|
|
299
|
-
### Additional API
|
|
300
|
-
|
|
301
|
-
This API is where Pulseq-zero comes into play:
|
|
302
|
-
|
|
303
|
-
In the `mr0_mode` context, all PyPulseq functions are swapped for Pulseq-zero implementations that track all calls so that the sequence can be converted to MR-zero later:
|
|
304
|
-
```python
|
|
305
|
-
with pulseqzero.mr0_mode():
|
|
306
|
-
seq = build_my_seq()
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
If you use some functions that are only available in PyPulseq but not Pulseq-zero (PNS computations or similar), you can check if `mr0_mode` is activated:
|
|
310
|
-
```python
|
|
311
|
-
if pulseqzero.is_mr0_mode():
|
|
312
|
-
pass
|
|
313
|
-
else:
|
|
314
|
-
seq.calculate_pns()
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
Finally, the sequence object returned in `mr0_mode` has one method that is not available otherwise and forms the central point of Pulseq-zero:
|
|
318
|
-
```python
|
|
319
|
-
with pulseqzero.mr0_mode():
|
|
320
|
-
seq = build_my_seq()
|
|
321
|
-
# This function here
|
|
322
|
-
mr0_seq = seq.to_mr0()
|
|
323
|
-
# Now do some simulations with mr0_seq!
|
|
324
|
-
```
|
|
325
|
-
|
|
326
342
|
|
|
327
343
|
## 5. References
|
|
328
344
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|