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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pulseqzero
3
- Version: 0.1.2
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 [pulseqzero/seq_examples](pulseqzero/seq_examples).
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 zeroes as mr0 mode doesn't store pulse shapes |
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 [pulseqzero/seq_examples](pulseqzero/seq_examples).
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 zeroes as mr0 mode doesn't store pulse shapes |
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pulseqzero"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Use PyPulseq for MRI sequence definitions in MR-zero"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -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
- self.make_arbitrary_grad = pp.make_arbitrary_grad
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
@@ -28,7 +28,7 @@ def calc_rf_bandwidth(rf, cutoff=0.5, return_axis=False, return_spectrum=False):
28
28
 
29
29
 
30
30
  def calc_rf_center(rf):
31
- return 0, 0
31
+ return rf.shape_dur / 2, 0
32
32
 
33
33
 
34
34
  def get_supported_labels():
@@ -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.ndarray([0, grad.rise_time])
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.ndarray([grad.rise_time, grad.rise_time + grad.flat_time])
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.ndarray([grad.rise_time + grad.flat_time, total_duration])
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(area, max_slew, max_grad)
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(area, max_slew, max_grad)
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", max_grad=max_grad, max_slew=max_slew,
229
- flat_time=duration, flat_area=area
228
+ "z", system=system,
229
+ flat_area=area, flat_time=duration
230
230
  )
231
231
  gzr = make_trapezoid(
232
- "z", max_grad=max_grad, max_slew=max_slew,
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 delay is None and adc is None
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
- assert delay is None
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
- def parse_adc(adc: Adc, grad_x, grad_y, grad_z) -> tuple[TmpAdc, TmpSpoiler]:
160
- duration = calc_duration(adc, grad_x, grad_y, grad_z)
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
- return torch.heaviside(torch.as_tensor(x), torch.tensor(0.5))
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.times[:-1])
225
- t2 = torch.as_tensor(grad.times[1:])
226
- c1 = torch.as_tensor(grad.amplitudes[:-1])
227
- c2 = torch.as_tensor(grad.amplitudes[1:])
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 = len(filter(lambda b: len(b) > 0, self.blocks))
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.2
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 [pulseqzero/seq_examples](pulseqzero/seq_examples).
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 zeroes as mr0 mode doesn't store pulse shapes |
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
 
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/pulseqzero/__init__.py
5
+ src/pulseqzero/math.py
5
6
  src/pulseqzero.egg-info/PKG-INFO
6
7
  src/pulseqzero.egg-info/SOURCES.txt
7
8
  src/pulseqzero.egg-info/dependency_links.txt
File without changes
File without changes