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 ADDED
@@ -0,0 +1,92 @@
1
+ from contextlib import contextmanager
2
+
3
+
4
+ class Impl:
5
+ def __init__(self):
6
+ self.use_pypulseq()
7
+
8
+ def use_pypulseq(self):
9
+ import pypulseq as pp
10
+ self.calc_SAR = pp.calc_SAR
11
+ self.Sequence = pp.Sequence
12
+ self.add_gradients = pp.add_gradients
13
+ self.align = pp.align
14
+ self.calc_duration = pp.calc_duration
15
+ self.calc_ramp = pp.calc_ramp
16
+ self.calc_rf_bandwidth = pp.calc_rf_bandwidth
17
+ self.calc_rf_center = pp.calc_rf_center
18
+ self.make_adc = pp.make_adc
19
+ self.make_adiabatic_pulse = pp.make_adiabatic_pulse
20
+ self.make_arbitrary_grad = pp.make_arbitrary_grad
21
+ self.make_arbitrary_rf = pp.make_arbitrary_rf
22
+ self.make_block_pulse = pp.make_block_pulse
23
+ self.sigpy_n_seq = pp.sigpy_n_seq
24
+ self.make_slr = pp.make_slr
25
+ self.make_sms = pp.make_sms
26
+ self.make_delay = pp.make_delay
27
+ self.make_digital_output_pulse = pp.make_digital_output_pulse
28
+ self.make_extended_trapezoid = pp.make_extended_trapezoid
29
+ self.make_extended_trapezoid_area = pp.make_extended_trapezoid_area
30
+ self.make_gauss_pulse = pp.make_gauss_pulse
31
+ self.make_label = pp.make_label
32
+ self.make_sinc_pulse = pp.make_sinc_pulse
33
+ self.make_trapezoid = pp.make_trapezoid
34
+ self.SigpyPulseOpts = pp.SigpyPulseOpts
35
+ self.make_trigger = pp.make_trigger
36
+ self.Opts = pp.Opts
37
+ self.points_to_waveform = pp.points_to_waveform
38
+ self.rotate = pp.rotate
39
+ self.scale_grad = pp.scale_grad
40
+ self.split_gradient = pp.split_gradient
41
+ self.split_gradient_at = pp.split_gradient_at
42
+ self.get_supported_labels = pp.get_supported_labels
43
+ self.traj_to_grad = pp.traj_to_grad
44
+
45
+ def use_pulseqzero(self):
46
+ from . import adapter as ad
47
+ self.calc_SAR = ad.calc_SAR
48
+ self.Sequence = ad.Sequence
49
+ # self.add_gradients = ad.add_gradients
50
+ # self.align = ad.align
51
+ self.calc_duration = ad.calc_duration
52
+ # self.calc_ramp = ad.calc_ramp
53
+ self.calc_rf_bandwidth = ad.calc_rf_bandwidth
54
+ self.calc_rf_center = ad.calc_rf_center
55
+ self.make_adc = ad.make_adc
56
+ # self.make_adiabatic_pulse = ad.make_adiabatic_pulse
57
+ self.make_arbitrary_grad = ad.make_arbitrary_grad
58
+ self.make_arbitrary_rf = ad.make_arbitrary_rf
59
+ self.make_block_pulse = ad.make_block_pulse
60
+ # self.sigpy_n_seq = ad.sigpy_n_seq
61
+ # self.make_slr = ad.make_slr
62
+ # self.make_sms = ad.make_sms
63
+ self.make_delay = ad.make_delay
64
+ self.make_digital_output_pulse = ad.make_digital_output_pulse
65
+ self.make_extended_trapezoid = ad.make_extended_trapezoid
66
+ # self.make_extended_trapezoid_area = ad.make_extended_trapezoid_area
67
+ self.make_gauss_pulse = ad.make_gauss_pulse
68
+ self.make_label = ad.make_label
69
+ self.make_sinc_pulse = ad.make_sinc_pulse
70
+ self.make_trapezoid = ad.make_trapezoid
71
+ # self.SigpyPulseOpts = ad.SigpyPulseOpts
72
+ self.make_trigger = ad.make_trigger
73
+ self.Opts = ad.Opts
74
+ # self.points_to_waveform = ad.points_to_waveform
75
+ # self.rotate = ad.rotate
76
+ # self.scale_grad = ad.scale_grad
77
+ # self.split_gradient = ad.split_gradient
78
+ # self.split_gradient_at = ad.split_gradient_at
79
+ self.get_supported_labels = ad.get_supported_labels
80
+ # self.traj_to_grad = ad.traj_to_grad
81
+
82
+
83
+ pp_impl = Impl()
84
+
85
+
86
+ @contextmanager
87
+ def mr0_mode():
88
+ pp_impl.use_pulseqzero()
89
+ try:
90
+ yield
91
+ finally:
92
+ pp_impl.use_pypulseq()
@@ -0,0 +1,47 @@
1
+ def calc_SAR(file):
2
+ pass
3
+
4
+
5
+ def make_label(label, type, value):
6
+ pass
7
+
8
+
9
+ def calc_duration(*args):
10
+ # We assume that all events have a duration property
11
+ return max(
12
+ (event.duration for event in args if event is not None),
13
+ default=0.0
14
+ )
15
+
16
+
17
+ def calc_rf_bandwidth(rf, cutoff=0.5, return_axis=False, return_spectrum=False):
18
+ import numpy as np
19
+ bw = 0
20
+ spectrum = np.zeros(1)
21
+ w = np.zeros(1)
22
+
23
+ if return_spectrum and not return_axis:
24
+ return bw, spectrum
25
+ if return_axis:
26
+ return bw, spectrum, w
27
+ return bw
28
+
29
+
30
+ def calc_rf_center(rf):
31
+ return 0, 0
32
+
33
+
34
+ def get_supported_labels():
35
+ return (
36
+ "SLC", "SEG", "REP", "AVG", "SET", "ECO", "PHS", "LIN", "PAR", "NAV",
37
+ "REV", "SMS", "REF", "IMA", "NOISE", "PMC", "NOROT", "NOPOS", "NOSCL",
38
+ "ONCE", "TRID",
39
+ )
40
+
41
+
42
+ from .opts import Opts
43
+ from .delay import make_delay, make_trigger, make_digital_output_pulse
44
+ from .adc import make_adc
45
+ from .grads import make_trapezoid, make_arbitrary_grad, make_extended_trapezoid
46
+ from .pulses import make_arbitrary_rf, make_block_pulse, make_gauss_pulse, make_sinc_pulse
47
+ from .sequence import Sequence
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass
2
+ from ..adapter import Opts
3
+
4
+
5
+ def make_adc(
6
+ num_samples,
7
+ delay=0,
8
+ duration=None,
9
+ dwell=None,
10
+ freq_offset=0,
11
+ phase_offset=0,
12
+ system=None
13
+ ):
14
+ if (dwell is None) == (duration is None):
15
+ raise ValueError("Either dwell or duration must be defined")
16
+
17
+ if dwell is None:
18
+ dwell = duration / num_samples
19
+ if system is None:
20
+ system = Opts.default
21
+ if delay < system.adc_dead_time:
22
+ delay = system.adc_dead_time
23
+
24
+ return Adc(num_samples, dwell, delay, freq_offset, phase_offset)
25
+
26
+
27
+ @dataclass
28
+ class Adc:
29
+ num_samples: ...
30
+ dwell: ...
31
+ delay: ...
32
+ freq_offset: ... # ignored by sim
33
+ phase_offset: ...
34
+
35
+ @property
36
+ def duration(self):
37
+ return self.delay + self.num_samples * self.dwell
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ def make_delay(d):
5
+ return Delay(d)
6
+
7
+
8
+ def make_trigger(channel, delay=0, duration=0, system=None):
9
+ return Delay(delay)
10
+
11
+
12
+ def make_digital_output_pulse(channel, delay=0, duration=None, system=None):
13
+ return Delay(delay)
14
+
15
+
16
+ @dataclass
17
+ class Delay:
18
+ delay: ...
19
+
20
+ @property
21
+ def duration(self):
22
+ return self.delay
@@ -0,0 +1,222 @@
1
+ from dataclasses import dataclass
2
+ from copy import copy
3
+ import numpy as np
4
+ from ..adapter import Opts, calc_duration
5
+
6
+
7
+ def scale_grad(grad, scale):
8
+ grad = copy(grad)
9
+ if isinstance(grad, TrapGrad):
10
+ grad.amplitude *= scale
11
+ if isinstance(grad, FreeGrad):
12
+ grad.waveform *= scale
13
+ return grad
14
+
15
+
16
+ def split_gradient(grad, system):
17
+ assert isinstance(grad, TrapGrad)
18
+ if system is None:
19
+ system = Opts.default
20
+ total_duration = calc_duration(grad)
21
+
22
+ ramp_up = make_extended_trapezoid(
23
+ channel=grad.channel,
24
+ amplitudes=np.array([0, grad.amplitude]),
25
+ times=np.ndarray([0, grad.rise_time])
26
+ )
27
+ flat_top = make_extended_trapezoid(
28
+ channel=grad.channel,
29
+ amplitudes=np.array([grad.amplitude, grad.amplitude]),
30
+ times=np.ndarray([grad.rise_time, grad.rise_time + grad.flat_time])
31
+ )
32
+ ramp_down = make_extended_trapezoid(
33
+ channel=grad.channel,
34
+ amplitudes=np.array([grad.amplitude, 0]),
35
+ times=np.ndarray([grad.rise_time + grad.flat_time, total_duration])
36
+ )
37
+
38
+ return ramp_up, flat_top, ramp_down
39
+
40
+
41
+ def make_trapezoid(
42
+ channel,
43
+ amplitude=None,
44
+ area=None,
45
+ delay=0,
46
+ duration=None,
47
+ fall_time=None,
48
+ flat_area=None,
49
+ flat_time=None,
50
+ max_grad=None,
51
+ max_slew=None,
52
+ rise_time=None,
53
+ system=None,
54
+ ):
55
+ if system is None:
56
+ system = Opts.default
57
+ if max_grad is None:
58
+ max_grad = system.max_grad
59
+ if max_slew is None:
60
+ max_slew = system.max_slew
61
+
62
+ # new_amp is only calculated to set rise_time below, the actual
63
+ # amplitude is then calculated from tmp_amp and the timing
64
+
65
+ # TODO: This function should really be split into multiple with the different argument combination options
66
+
67
+ if flat_time is not None:
68
+ if amplitude is not None:
69
+ new_amp = amplitude
70
+ elif area is not None:
71
+ assert rise_time is not None
72
+ if fall_time is None:
73
+ fall_time = rise_time
74
+ new_amp = area / (rise_time / 2 + flat_time + fall_time / 2)
75
+ else:
76
+ assert flat_area is not None
77
+ new_amp = flat_area / flat_time
78
+
79
+ if rise_time is None:
80
+ rise_time = abs(new_amp) / max_slew
81
+ if fall_time is None:
82
+ fall_time = rise_time
83
+
84
+ elif duration is not None:
85
+ if amplitude is None:
86
+ assert area is not None
87
+
88
+ if rise_time is None:
89
+ _, rise_time, flat_time, fall_time = calc_params_for_area(area, max_slew, max_grad)
90
+ assert duration >= rise_time + flat_time + fall_time
91
+
92
+ dC = 1 / abs(2 * max_slew)
93
+ new_amp = (
94
+ duration - (duration**2 - 4 * abs(area) * dC)
95
+ ) / (2 * dC)
96
+ else:
97
+ if fall_time is None:
98
+ fall_time = rise_time
99
+ new_amp = area / (duration - rise_time / 2 - fall_time / 2)
100
+ else:
101
+ new_amp = amplitude
102
+
103
+ if rise_time is None:
104
+ rise_time = abs(new_amp) / max_slew
105
+ if fall_time is None:
106
+ fall_time = rise_time
107
+ flat_time = duration - rise_time - fall_time
108
+
109
+ if amplitude is None:
110
+ new_amp = area / (rise_time / 2 + flat_time + fall_time / 2)
111
+
112
+ else:
113
+ assert area is not None
114
+ new_amp, rise_time, flat_time, fall_time = calc_params_for_area(area, max_slew, max_grad)
115
+
116
+ return TrapGrad(
117
+ channel,
118
+ new_amp,
119
+ rise_time,
120
+ flat_time,
121
+ fall_time,
122
+ delay
123
+ )
124
+
125
+
126
+ def calc_params_for_area(area, max_slew, max_grad):
127
+ rise_time = (abs(area) / max_slew)**0.5
128
+ amplitude = area / rise_time
129
+ t_eff = rise_time
130
+
131
+ if abs(amplitude) > max_grad:
132
+ t_eff = abs(area) / max_grad
133
+ amplitude = area / t_eff
134
+ rise_time = abs(amplitude) / max_slew
135
+
136
+ flat_time = t_eff - rise_time
137
+ fall_time = rise_time
138
+
139
+ return amplitude, rise_time, flat_time, fall_time
140
+
141
+
142
+ @dataclass
143
+ class TrapGrad:
144
+ channel: ...
145
+ amplitude: ...
146
+ rise_time: ...
147
+ flat_time: ...
148
+ fall_time: ...
149
+ delay: ...
150
+
151
+ @property
152
+ def area(self):
153
+ return self.amplitude * (self.rise_time / 2 + self.flat_time + self.fall_time / 2)
154
+
155
+ @property
156
+ def flat_area(self):
157
+ return self.amplitude * self.flat_time
158
+
159
+ @property
160
+ def duration(self):
161
+ return self.delay + self.rise_time + self.flat_time + self.fall_time
162
+
163
+
164
+ def make_arbitrary_grad(
165
+ channel,
166
+ waveform,
167
+ delay=0,
168
+ max_grad=None,
169
+ max_slew=None,
170
+ system=None,
171
+ ):
172
+ if system is None:
173
+ system = Opts.default
174
+
175
+ tt = (np.arange(len(waveform)) + 0.5) * system.grad_raster_time
176
+
177
+ return FreeGrad(
178
+ channel,
179
+ waveform,
180
+ delay,
181
+ tt,
182
+ len(waveform) * system.grad_raster_time
183
+ )
184
+
185
+
186
+ @dataclass
187
+ class FreeGrad:
188
+ channel: ...
189
+ waveform: ...
190
+ delay: ...
191
+ tt: ...
192
+ shape_dur: ...
193
+
194
+ @property
195
+ def duration(self):
196
+ return self.delay + self.shape_dur
197
+
198
+ @property
199
+ def area(self):
200
+ return 0.5 * (
201
+ (self.tt[1:] - self.tt[:-1]) *
202
+ (self.waveform[1:] + self.waveform[:-1])
203
+ ).sum()
204
+
205
+
206
+ def make_extended_trapezoid(
207
+ channel,
208
+ amplitudes=np.zeros(1),
209
+ convert_to_arbitrary=False,
210
+ max_grad=None,
211
+ max_slew=None,
212
+ skip_check=False,
213
+ system=None,
214
+ times=np.zeros(1),
215
+ ):
216
+ return FreeGrad(
217
+ channel,
218
+ amplitudes,
219
+ times[0],
220
+ times - times[0],
221
+ times[-1]
222
+ )
@@ -0,0 +1,98 @@
1
+ def convert(from_value, from_unit, gamma, to_unit=""):
2
+ from math import pi
3
+
4
+ value_SI = from_value * {
5
+ "Hz/m": 1,
6
+ "mT/m": 1e-3 * gamma,
7
+ "rad/ms/mm": 1e6 / (2 * pi),
8
+ "Hz/m/s": 1,
9
+ "mT/m/ms": gamma,
10
+ "T/m/s": gamma,
11
+ "rad/ms/mm/ms": 1e9 / (2 * pi),
12
+ }[from_unit]
13
+
14
+ return value_SI * {
15
+ "Hz/m": 1,
16
+ "mT/m": 1e3 / gamma,
17
+ "rad/ms/mm": 1e-6 * 2 * pi,
18
+ "Hz/m/s": 1,
19
+ "mT/m/ms": 1 / gamma,
20
+ "T/m/s": 1 / gamma,
21
+ "rad/ms/mm/ms": 1e-9 * 2 * pi
22
+ }[to_unit]
23
+
24
+
25
+ class Opts:
26
+ def __init__(
27
+ self,
28
+ adc_dead_time=None,
29
+ adc_raster_time=None,
30
+ block_duration_raster=None,
31
+ gamma=None,
32
+ grad_raster_time=None,
33
+ grad_unit="Hz/m",
34
+ max_grad=None,
35
+ max_slew=None,
36
+ rf_dead_time=None,
37
+ rf_raster_time=None,
38
+ rf_ringdown_time=None,
39
+ rise_time=None,
40
+ slew_unit="Hz/m/s",
41
+ B0=None,
42
+ ):
43
+ def select(a, b):
44
+ if a is not None:
45
+ return a
46
+ else:
47
+ return getattr(self.default, b)
48
+ return a if a is not None else b
49
+
50
+ self.gamma = select(gamma, "gamma")
51
+ self.max_grad = select(convert(max_grad, grad_unit, self.gamma, "Hz/m"), "max_grad")
52
+ self.max_slew = select(convert(max_slew, slew_unit, self.gamma, "Hz/m/s"), "max_slew")
53
+ # Rise time seems to overwrite
54
+ if rise_time is not None:
55
+ self.max_slew = self.max_grad / rise_time
56
+ self.adc_dead_time = select(adc_dead_time, "adc_dead_time")
57
+ self.adc_raster_time = select(adc_raster_time, "adc_raster_time")
58
+ self.block_duration_raster = select(block_duration_raster, "block_duration_raster")
59
+ self.rf_dead_time = select(rf_dead_time, "rf_dead_time")
60
+ self.rf_raster_time = select(rf_raster_time, "rf_raster_time")
61
+ self.grad_raster_time = select(grad_raster_time, "grad_raster_time")
62
+ self.rf_ringdown_time = select(rf_ringdown_time, "rf_ringdown_time")
63
+ self.B0 = select(B0, "B0")
64
+
65
+ def set_as_default(self):
66
+ from copy import copy
67
+ Opts.default = copy(self)
68
+
69
+ @classmethod
70
+ def reset_default(cls):
71
+ cls.default = Opts(
72
+ gamma=42.576e6,
73
+ max_grad=40,
74
+ grad_unit="mT/m",
75
+ max_slew=170,
76
+ slew_unit="T/m/s",
77
+ rf_dead_time=0,
78
+ rf_ringdown_time=0,
79
+ adc_dead_time=0,
80
+ adc_raster_time=100e-9,
81
+ rf_raster_time=1e-6,
82
+ grad_raster_time=10e-6,
83
+ block_duration_raster=10e-6,
84
+ B0=1.5
85
+ )
86
+
87
+ def __str__(self) -> str:
88
+ """
89
+ Print a string representation of the system limits objects.
90
+ """
91
+ variables = vars(self)
92
+ s = [f"{key}: {value}" for key, value in variables.items()]
93
+ s = "\n".join(s)
94
+ s = "System limits:\n" + s
95
+ return s
96
+
97
+
98
+ Opts.reset_default()