mrzerocore 0.4.3__cp37-abi3-musllinux_1_2_aarch64.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.
Files changed (41) hide show
  1. MRzeroCore/__init__.py +22 -0
  2. MRzeroCore/_prepass.abi3.so +0 -0
  3. MRzeroCore/phantom/brainweb/.gitignore +1 -0
  4. MRzeroCore/phantom/brainweb/__init__.py +192 -0
  5. MRzeroCore/phantom/brainweb/brainweb_data.json +92 -0
  6. MRzeroCore/phantom/brainweb/brainweb_data_sources.txt +74 -0
  7. MRzeroCore/phantom/brainweb/output/.gitkeep +0 -0
  8. MRzeroCore/phantom/custom_voxel_phantom.py +240 -0
  9. MRzeroCore/phantom/nifti_phantom.py +210 -0
  10. MRzeroCore/phantom/sim_data.py +200 -0
  11. MRzeroCore/phantom/tissue_dict.py +269 -0
  12. MRzeroCore/phantom/voxel_grid_phantom.py +610 -0
  13. MRzeroCore/pulseq/exporter.py +374 -0
  14. MRzeroCore/pulseq/exporter_v2.py +650 -0
  15. MRzeroCore/pulseq/helpers.py +228 -0
  16. MRzeroCore/pulseq/pulseq_exporter.py +553 -0
  17. MRzeroCore/pulseq/pulseq_loader/__init__.py +66 -0
  18. MRzeroCore/pulseq/pulseq_loader/adc.py +48 -0
  19. MRzeroCore/pulseq/pulseq_loader/helpers.py +75 -0
  20. MRzeroCore/pulseq/pulseq_loader/pulse.py +80 -0
  21. MRzeroCore/pulseq/pulseq_loader/pulseq_file/__init__.py +235 -0
  22. MRzeroCore/pulseq/pulseq_loader/pulseq_file/adc.py +68 -0
  23. MRzeroCore/pulseq/pulseq_loader/pulseq_file/block.py +98 -0
  24. MRzeroCore/pulseq/pulseq_loader/pulseq_file/definitons.py +68 -0
  25. MRzeroCore/pulseq/pulseq_loader/pulseq_file/gradient.py +70 -0
  26. MRzeroCore/pulseq/pulseq_loader/pulseq_file/helpers.py +156 -0
  27. MRzeroCore/pulseq/pulseq_loader/pulseq_file/rf.py +91 -0
  28. MRzeroCore/pulseq/pulseq_loader/pulseq_file/trap.py +69 -0
  29. MRzeroCore/pulseq/pulseq_loader/spoiler.py +33 -0
  30. MRzeroCore/reconstruction.py +104 -0
  31. MRzeroCore/sequence.py +747 -0
  32. MRzeroCore/simulation/isochromat_sim.py +254 -0
  33. MRzeroCore/simulation/main_pass.py +286 -0
  34. MRzeroCore/simulation/pre_pass.py +192 -0
  35. MRzeroCore/simulation/sig_to_mrd.py +362 -0
  36. MRzeroCore/util.py +884 -0
  37. MRzeroCore.libs/libgcc_s-39080030.so.1 +0 -0
  38. mrzerocore-0.4.3.dist-info/METADATA +121 -0
  39. mrzerocore-0.4.3.dist-info/RECORD +41 -0
  40. mrzerocore-0.4.3.dist-info/WHEEL +4 -0
  41. mrzerocore-0.4.3.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,553 @@
1
+ # -*- coding: utf-8 -*-
2
+ from types import SimpleNamespace
3
+ import sys
4
+ import numpy as np
5
+ import torch
6
+ from . import sequence as Seq
7
+ from . import util
8
+
9
+ sys.path.append("../scannerloop_libs")
10
+ from pypulseq.Sequence.sequence import Sequence
11
+ from pypulseq.make_adc import make_adc as _make_adc
12
+ from pypulseq.make_delay import make_delay as _make_delay
13
+ from pypulseq.make_sinc_pulse import make_sinc_pulse
14
+ from pypulseq.make_trap_pulse import make_trapezoid as _make_trapezoid
15
+ from pypulseq.make_gauss_pulse import make_gauss_pulse
16
+ from pypulseq.make_block_pulse import make_block_pulse
17
+ from pypulseq.opts import Opts
18
+
19
+
20
+ def rectify_flips(flips):
21
+ flip_angle = flips.angle.cpu()
22
+ flip_phase = flips.phase.cpu()
23
+
24
+ if flips.angle < 0:
25
+ flip_angle = -flips.angle
26
+ flip_phase = flips.phase + np.pi
27
+ flip_phase = torch.fmod(flip_phase, 2*np.pi)
28
+ return flip_angle.item(), flip_phase.item()
29
+
30
+
31
+ # Modified versions of make_delay, make_adc and make_trapezoid that ensure that
32
+ # all events (and thus gradients) are on the gradient time raster. If they are
33
+ # not, the scanner crashes without hinting why
34
+
35
+ def make_delay(d: float) -> SimpleNamespace:
36
+ """make_delay wrapper that rounds delay to the gradient time raster."""
37
+ return _make_delay(round(d / 10e-6) * 10e-6)
38
+
39
+
40
+ def make_adc(num_samples: int, system: Opts = Opts(), dwell: float = 0, duration: float = 0, delay: float = 0,
41
+ freq_offset: float = 0, phase_offset: float = 0) -> SimpleNamespace:
42
+ """make_adc wrapper that modifies the delay such that the total duration
43
+ is on the gradient time raster."""
44
+ # TODO: the total duration might not be on the gradient raster. If a
45
+ # sequence with optimized ADC durations fails the timing check, implement
46
+ # this functions to round the timing as necessary.
47
+
48
+ return _make_adc(
49
+ num_samples, system, dwell, duration, delay,
50
+ freq_offset, phase_offset
51
+ )
52
+
53
+
54
+ def make_trapezoid(channel: str, amplitude: float = 0, area: float = None, delay: float = 0, duration: float = 0,
55
+ flat_area: float = 0, flat_time: float = -1, max_grad: float = 0, max_slew: float = 0,
56
+ rise_time: float = 0, system: Opts = Opts()) -> SimpleNamespace:
57
+ """make_trapezoid wrapper that rounds gradients to the raster."""
58
+ raster = system.grad_raster_time
59
+ if delay != -1:
60
+ delay = round(delay / raster) * raster
61
+ if rise_time != -1:
62
+ rise_time = round(rise_time / raster) * raster
63
+ if flat_time != -1:
64
+ flat_time = round(flat_time / raster) * raster
65
+ if duration != -1:
66
+ duration = round(duration / raster) * raster
67
+
68
+ return _make_trapezoid(
69
+ channel, amplitude, area, delay, duration, flat_area, flat_time,
70
+ max_grad, max_slew, rise_time, system
71
+ )
72
+
73
+
74
+ nonsel = 0
75
+
76
+ def pulseq_write_EPG_3D(seq_param: Sequence,
77
+ path: str,
78
+ FOV: tuple[float, float, float],
79
+ plot_seq: bool = False,
80
+ write_data: int = 1):
81
+
82
+ """Export Sequence as pulseq file Version 1.3.1post1.
83
+
84
+ This function creates a pulseq file where the exported seq files matches
85
+ the simulation.
86
+ The SEQ file undergoes several modifications.
87
+ Firstly, the center of a pulse is placed at the beginning of the repetition.
88
+ To achieve this, the time of the last event within a repetition is reduced
89
+ by half the pulse length.
90
+ The gradient moment of the adc ramp (rising) is addtionally substracted
91
+ from the gradient in the event before an adc!
92
+ The gradient moment of the adc ramp (falling) is addtionally substracted
93
+ from the gradient in the event after an adc!
94
+ The simulation needs a rewinder of N_adc/2-1, since first a gradient is
95
+ played out, than the adc point is measured. To match this to the exporter
96
+ an additional half gradient moment of one adc needs to be added to the gradient
97
+ before and after an adc!
98
+ # Example for resolution 8! Rewinder goes to -5, after adc the gradient moment is +3.
99
+ # ADC wrong index / -4.5 -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 \
100
+ # Gradient Moment / -5 -4 -3 -2 -1 +0 +1 +2 +3 \
101
+ # Correct Gradient before adc by +deltakx/2!
102
+ # Correct Gradient after adc by -deltakx/2!
103
+ # ADC correct index / -4 -3 -2 -1 +0 +1 +2 +3 \
104
+ # Gradient Moment / -4.5 -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 +3.5 \
105
+
106
+ Warnings
107
+ --------
108
+ The exporter only operates successfully when the phase-encoding gradient
109
+ directly precedes the ADC.
110
+ Furthermore, gradients must also played out directly in the event after the ADCs,
111
+ so that they can be handled correctly.
112
+
113
+ Parameters
114
+ ----------
115
+ seq_param : Sequence
116
+ Sequence that will be exported as pulseq file.
117
+ path : String
118
+ Location where the pulseq file is saved.
119
+ FOV : tuple[float, float, float]
120
+ FOV of the sequence.
121
+ plot_seq : bool
122
+ Plot the sequences.
123
+ write_data : int
124
+ Writes the encoding of the sequence as command into the pulseq file.
125
+ This is needed for easier reconstruction of measurements.
126
+
127
+
128
+ Returns
129
+ -------
130
+ No return!
131
+ """
132
+
133
+
134
+ bdw_start = 0
135
+
136
+ # save pulseq definition
137
+ # slice_thickness = np.max([8e-3,5e-3*num_slices])
138
+ MAXSLEW = 200
139
+ FOV = [item / 1000 for item in FOV]
140
+ deltakx = 1.0 / FOV[0] # /(2*np.pi) before v.2.1.0
141
+ deltaky = 1.0 / FOV[1]
142
+ deltakz = 1.0 / FOV[2]
143
+
144
+ kwargs_for_opts = {"rf_ringdown_time": 20e-6, "rf_dead_time": 100e-6, "adc_dead_time": 20e-6, "max_grad": 80, "grad_unit": "mT/m", "max_slew": MAXSLEW, "slew_unit": "T/m/s"}
145
+ system = Opts(**kwargs_for_opts)
146
+ seq = Sequence(system)
147
+
148
+ seq.set_definition("FOV", FOV)
149
+ seq.add_block(make_delay(5.0))
150
+
151
+ # import pdb; pdb.set_trace()
152
+ for i,rep in enumerate(seq_param): # Loop over all repetitions
153
+ adc_start = 0
154
+ flip_angle,flip_phase = rectify_flips(rep.pulse)
155
+
156
+ for event in range(rep.event_count): # Loop over all events within one repetitions
157
+
158
+ # No adc in event
159
+ if torch.abs(rep.adc_usage[event]) == 0:
160
+
161
+ RFdurafter = 0
162
+ RFdurbefore = 0
163
+
164
+ # First event
165
+ if event == 0:
166
+
167
+ ###############################
168
+ ## global pulse
169
+ if rep.pulse.usage == Seq.PulseUsage.UNDEF:
170
+ RFdur = 0
171
+ if torch.abs(rep.pulse.angle) > 1e-8:
172
+
173
+ RFdur = 1*1e-3
174
+ kwargs_for_block = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "phase_offset": flip_phase}
175
+ rf_ex = make_block_pulse(**kwargs_for_block)
176
+
177
+ seq.add_block(rf_ex)
178
+ seq.add_block(make_delay(1e-4))
179
+
180
+ RFdurafter = 1e-4 + RFdur/2 + rf_ex.ringdown_time
181
+
182
+ elif (rep.pulse.usage == Seq.PulseUsage.EXCIT or
183
+ rep.pulse.usage == Seq.PulseUsage.STORE):
184
+ ###############################
185
+ ### excitation pulse
186
+ RFdur = 0
187
+ if torch.abs(rep.pulse.angle) > 1e-8:
188
+
189
+ if nonsel:
190
+ RFdur = 1*1e-3
191
+ kwargs_for_block = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "phase_offset": flip_phase}
192
+ rf_ex = make_block_pulse(**kwargs_for_block)
193
+ seq.add_block(rf_ex)
194
+ seq.add_block(make_delay(1e-4))
195
+
196
+ RFdurafter = 1e-4 + RFdur/2 + rf_ex.ringdown_time
197
+
198
+ else:
199
+ # alternatively slice selective:
200
+ RFdur = 1*1e-3
201
+ #kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": slice_thickness, "apodization": 0.5, "time_bw_product": 4, "phase_offset": flip_phase}
202
+ #rf_ex, gz, gzr= make_sinc_pulse(**kwargs_for_sinc)
203
+ kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": FOV[2], "apodization": 0.15, "time_bw_product": 2, "phase_offset": flip_phase, "return_gz": True}
204
+ rf_ex, gz, gzr = make_sinc_pulse(**kwargs_for_sinc)
205
+
206
+ #satPulse = mr.makeSincPulse(fa_sat, 'Duration', tp, 'system', seq.sys,'timeBwProduct', 2,'apodization', 0.15); % philips-like sinc
207
+ #%satPulse = mr.makeGaussPulse(fa_sat, 'Duration', t_p,'system',lims,'timeBwProduct', 0.2,'apodization', 0.5); % siemens-like gauss
208
+
209
+ seq.add_block(gzr)
210
+ seq.add_block(rf_ex, gz)
211
+ seq.add_block(gzr)
212
+
213
+ RFdurafter = gz.flat_time/2 + gz.fall_time + gzr.rise_time + gzr.flat_time + gzr.fall_time
214
+
215
+ elif rep.pulse.usage == Seq.PulseUsage.REFOC:
216
+ ###############################
217
+ ### refocusing pulse
218
+ RFdur = 0
219
+
220
+ if torch.abs(rep.pulse.angle) > 1e-8:
221
+ RFdur = 1*1e-3
222
+
223
+ if nonsel:
224
+ kwargs_for_block = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "phase_offset": flip_phase}
225
+ rf_ref = make_block_pulse(**kwargs_for_block)
226
+ seq.add_block(rf_ref)
227
+ seq.add_block(make_delay(1e-4))
228
+
229
+ RFdurafter = 1e-4 + RFdur/2 + rf_ref.ringdown_time
230
+ else:
231
+
232
+ kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": FOV[2], "apodization": 0.5, "time_bw_product": 4, "phase_offset": flip_phase, "return_gz": True}
233
+ rf_ref, gz_ref, gzr = make_sinc_pulse(**kwargs_for_sinc)
234
+ seq.add_block(gzr)
235
+ seq.add_block(rf_ref, gz_ref)
236
+ seq.add_block(gzr)
237
+
238
+ RFdurafter = gz_ref.flat_time/2 + gz_ref.fall_time + gzr.rise_time + gzr.flat_time + gzr.fall_time
239
+
240
+ elif rep.pulse.usage == Seq.PulseUsage.FATSAT:
241
+ ###############################
242
+ ### fat saturation pulse
243
+ RFdur = 6.120*1e-3
244
+ dCurrFrequency = 123.2
245
+ kwargs_for_gauss = {"flip_angle": 110*np.pi/180, "system": system, "slice_thickness": FOV[2], "duration": RFdur, "freq_offset": -3.3*dCurrFrequency, "time_bw_product": 0.2, "apodization": 0.5, "return_gz": True} # "bandwidth": 200
246
+ rf_ex, gz, gzr = make_gauss_pulse(**kwargs_for_gauss)
247
+ seq.add_block(gzr)
248
+ seq.add_block(rf_ex, gz)
249
+ seq.add_block(gzr)
250
+
251
+ RFdurafter = gz.flat_time/2 + gz.fall_time + gzr.rise_time + gzr.flat_time + gzr.fall_time
252
+
253
+
254
+ # Last event in one repetition, except the last repetition at all!
255
+ # Calculation of RF duration of the following repetition
256
+
257
+ elif event == rep.event_count - 1 and i < len(seq_param) - 2:
258
+
259
+ if seq_param[i+1].pulse.usage == Seq.PulseUsage.UNDEF or nonsel:
260
+ RFdur = 0
261
+ if torch.abs(rep.pulse.angle) > 1e-8:
262
+ RFdur = 1*1e-3
263
+ kwargs_for_block = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "phase_offset": flip_phase}
264
+ rf_ex = make_block_pulse(**kwargs_for_block)
265
+ RFdurbefore = rf_ex.delay + RFdur/2
266
+
267
+ elif (seq_param[i+1].pulse.usage == Seq.PulseUsage.EXCIT or
268
+ seq_param[i+1].pulse.usage == Seq.PulseUsage.STORE):
269
+ RFdur = 0
270
+ if torch.abs(rep.pulse.angle) > 1e-8:
271
+ RFdur = 1*1e-3
272
+ kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": FOV[2], "apodization": 0.15, "time_bw_product": 2, "phase_offset": flip_phase, "return_gz": True}
273
+ rf_ex, gz, gzr = make_sinc_pulse(**kwargs_for_sinc)
274
+ RFdurbefore = gzr.rise_time + gzr.flat_time + gzr.fall_time + gz.delay + gz.rise_time + gz.flat_time/2
275
+
276
+ elif seq_param[i+1].pulse.usage == Seq.PulseUsage.REFOC:
277
+ RFdur = 0
278
+ if torch.abs(rep.pulse.angle) > 1e-8:
279
+ RFdur = 1*1e-3
280
+ kwargs_for_sinc = {"flip_angle": flip_angle, "system": system, "duration": RFdur, "slice_thickness": FOV[2], "apodization": 0.5, "time_bw_product": 4, "phase_offset": flip_phase, "return_gz": True}
281
+ rf_ref, gz_ref, gzr = make_sinc_pulse(**kwargs_for_sinc)
282
+ RFdurbefore = gzr.rise_time + gzr.flat_time + gzr.fall_time + gz_ref.delay + gz_ref.rise_time + gz_ref.flat_time/2
283
+
284
+ elif seq_param[i+1].pulse.usage == Seq.PulseUsage.FATSAT:
285
+ ###############################
286
+ ### fat saturation pulse
287
+ RFdur = 6.120*1e-3
288
+ dCurrFrequency = 123.2
289
+ kwargs_for_gauss = {"flip_angle": 110*np.pi/180, "system": system, "slice_thickness": FOV[2], "duration": RFdur, "freq_offset": -3.3*dCurrFrequency, "time_bw_product": 0.2, "apodization": 0.5, "return_gz": True} # "bandwidth": 200
290
+ rf_ex, gz, gzr= make_gauss_pulse(**kwargs_for_gauss)
291
+ RFdurbefore = gzr.rise_time + gzr.flat_time + gzr.fall_time + gz.delay + gz.rise_time + gz.flat_time/2
292
+
293
+ # Substract the time of the rf pulse starting from the middle of the rf pulse to match simulation
294
+ dur = rep.event_time[event].item() - RFdurafter
295
+
296
+ # Substract the time of the rf pulse of the following repetition
297
+ if event == rep.event_count - 1: # Last event in rep
298
+ dur -= RFdurbefore
299
+
300
+ if dur < 0:
301
+ raise Exception('Event Time too short! Event Time: Rep: '+ str(i) + ', Event: ' +str(event),', increase event_time by at least: ' + str(-dur))
302
+
303
+ gx_gradmom = rep.gradm[event,0].item()*deltakx
304
+ gy_gradmom = rep.gradm[event,1].item()*deltaky
305
+ gz_gradmom = rep.gradm[event,2].item()*deltakz
306
+ idx_T = np.nonzero(torch.abs(rep.adc_usage))
307
+ dur_adc = np.round(torch.sum(rep.event_time[idx_T],0).item(),decimals=5)
308
+
309
+ if np.abs(gx_gradmom)>0:
310
+ gx_adc_ramp = 0
311
+ if any(rep.adc_usage):
312
+ # Case: Gradient before adc
313
+ # Correct duration by the rise time
314
+ # Correct gradient by the gradient moment of adc ramp
315
+ if event < rep.event_count - 1 and torch.abs(rep.adc_usage[event+1]) and torch.abs(rep.gradm[event+1,0]) > 0:
316
+
317
+ # ADC wrong / -4.5 -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 \
318
+ # Gradient Moment / -5 -4 -3 -2 -1 +0 +1 +2 +3 \
319
+ # Correct Gradient before adc by +0.5 --> neg sign needed because the gradient correction is substracted (gx_gradmom-gx_adc_ramp)!
320
+ # Correct Gradient after adc by -0.5 --> pos sign is needed because the gradient correction is substracted (gx_gradmom-gx_adc_ramp)!
321
+ # ADC correct / -4 -3 -2 -1 +0 +1 +2 +3 \
322
+ # Gradient Moment / -4.5 -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 +3.5 \
323
+
324
+ kwargs_for_gx = {"channel": 'x', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,0]).item()*deltakx, "flat_time": dur_adc}
325
+ gx_adc = make_trapezoid(**kwargs_for_gx)
326
+ gx_adc_ramp = gx_adc.amplitude*gx_adc.rise_time/2 - torch.sign(rep.gradm[event+1,0]).item() * deltakx/2 # Correct gradient by half of the gradient moment of one adc moment!
327
+ gx_rise_time = gx_adc.rise_time
328
+ dur -= gx_rise_time # Correct timing for rise and fall time of adc gradient
329
+ # Case: Gradient after adc
330
+ # Correct duration by the fall time
331
+ # Correct gradient by the gradient moment of adc ramp
332
+ elif event > 0 and torch.abs(rep.adc_usage[event-1]) and torch.abs(rep.gradm[event-1,0]) > 0: # Event after adc to reduce dur by fall time of adc gradient
333
+ kwargs_for_gx = {"channel": 'x', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,0]).item()*deltakx, "flat_time": dur_adc}
334
+ gx_adc = make_trapezoid(**kwargs_for_gx)
335
+ gx_adc_ramp = gx_adc.amplitude*gx_adc.fall_time/2 + torch.sign(rep.gradm[event-1,0]).item() * deltakx/2
336
+ gx_fall_time = gx_adc.fall_time
337
+ dur -= gx_fall_time # Correct timing for rise and fall time of adc gradient
338
+ kwargs_for_gx = {"channel": 'x', "system": system, "area": gx_gradmom-gx_adc_ramp, "duration": dur}
339
+ try:
340
+ gx = make_trapezoid(**kwargs_for_gx)
341
+ except:
342
+ raise Exception('Event Time too short (gx)! Event Time: Rep: ' + str(i) + ', Event: ' +str(event))
343
+ if np.abs(gy_gradmom)>0:
344
+ gy_adc_ramp = 0
345
+ if any(rep.adc_usage):
346
+ # Case: Gradient before adc
347
+ # Correct duration by the rise time
348
+ # Correct gradient by the gradient moment of adc ramp
349
+ if event < rep.event_count - 1 and torch.abs(rep.adc_usage[event+1]) and torch.abs(rep.gradm[event+1,1]) > 0:
350
+ kwargs_for_gy = {"channel": 'y', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,1]).item()*deltaky, "flat_time": dur_adc}
351
+ gy_adc = make_trapezoid(**kwargs_for_gy)
352
+ gy_adc_ramp = gy_adc.amplitude*gy_adc.rise_time/2 - torch.sign(rep.gradm[event+1,1]).item() * deltaky/2
353
+ gy_rise_time = gy_adc.rise_time
354
+ dur -= gy_rise_time # Correct timing for rise and fall time of adc gradient
355
+ # Case: Gradient after adc
356
+ # Correct duration by the fall time
357
+ # Correct gradient by the gradient moment of adc ramp
358
+ elif event > 0 and torch.abs(rep.adc_usage[event-1]) and torch.abs(rep.gradm[event-1,1]) > 0: # Event after adc to reduce dur by fall time of adc gradient
359
+ kwargs_for_gy = {"channel": 'y', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,1]).item()*deltaky, "flat_time": dur_adc}
360
+ gy_adc = make_trapezoid(**kwargs_for_gy)
361
+ gy_adc_ramp = gy_adc.amplitude*gy_adc.fall_time/2 + torch.sign(rep.gradm[event-1,1]).item() * deltaky/2
362
+ gy_fall_time = gy_adc.fall_time
363
+ dur -= gy_fall_time # Correct timing for rise and fall time of adc gradient
364
+ kwargs_for_gy = {"channel": 'y', "system": system, "area": gy_gradmom-gy_adc_ramp, "duration": dur}
365
+ try:
366
+ gy = make_trapezoid(**kwargs_for_gy)
367
+ except:
368
+ raise Exception('Event Time too short (gy)! Event Time: Rep: ' + str(i) + ', Event: ' +str(event))
369
+ if np.abs(gz_gradmom)>0:
370
+ gz_adc_ramp = 0
371
+ if any(rep.adc_usage):
372
+ # Case: Gradient before adc
373
+ # Correct duration by the rise time
374
+ # Correct gradient by the gradient moment of adc ramp
375
+ if event < rep.event_count - 1 and torch.abs(rep.adc_usage[event+1]) and torch.abs(rep.gradm[event+1,2]) > 0:
376
+ kwargs_for_gz = {"channel": 'z', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,2]).item()*deltakz, "flat_time": dur_adc}
377
+ gz_adc = make_trapezoid(**kwargs_for_gz)
378
+ gz_adc_ramp = gz_adc.amplitude*gz_adc.rise_time/2 - torch.sign(rep.gradm[event+1,2]).item() * deltakz/2
379
+ gz_rise_time = gz_adc.rise_time
380
+ dur -= gz_rise_time # Correct timing for rise and fall time of adc gradient
381
+ # Case: Gradient after adc
382
+ # Correct duration by the fall time
383
+ # Correct gradient by the gradient moment of adc ramp
384
+ elif event > 0 and torch.abs(rep.adc_usage[event-1]) and torch.abs(rep.gradm[event-1,2]) > 0: # Event after adc to reduce dur by fall time of adc gradient
385
+ kwargs_for_gz = {"channel": 'z', "system": system, "flat_area": torch.sum(rep.gradm[idx_T,2]).item()*deltakz, "flat_time": dur_adc}
386
+ gz_adc = make_trapezoid(**kwargs_for_gz)
387
+ gz_adc_ramp = gz_adc.amplitude*gz_adc.fall_time/2 + torch.sign(rep.gradm[event-1,2]).item() * deltakz/2
388
+ gz_fall_time = gz_adc.fall_time
389
+ dur -= gz_fall_time # Correct timing for rise and fall time of adc gradient
390
+ kwargs_for_gz = {"channel": 'z', "system": system, "area": gz_gradmom-gz_adc_ramp, "duration": dur}
391
+ try:
392
+ gz = make_trapezoid(**kwargs_for_gz)
393
+ except:
394
+ raise Exception('Event Time too short (gz)! Event Time: Rep: ' + str(i) + ', Event: ' +str(event))
395
+
396
+ if np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom):
397
+ seq.add_block(gx,gy,gz)
398
+ elif np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0:
399
+ seq.add_block(gx,gy)
400
+ elif np.abs(gx_gradmom) > 0 and np.abs(gz_gradmom) > 0:
401
+ seq.add_block(gx,gz)
402
+ elif np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom) > 0:
403
+ seq.add_block(gy,gz)
404
+ elif np.abs(gx_gradmom) > 0:
405
+ seq.add_block(gx)
406
+ elif np.abs(gy_gradmom) > 0:
407
+ seq.add_block(gy)
408
+ elif np.abs(gz_gradmom) > 0:
409
+ seq.add_block(gz)
410
+ else:
411
+ seq.add_block(make_delay(dur))
412
+ else: #adc mask == 1
413
+ if adc_start == 1:
414
+ pass
415
+ else:
416
+ adc_start = 1
417
+ if bdw_start == 0:
418
+ bwd = (1/rep.event_time[event])/torch.sum(torch.abs(rep.adc_usage)>0)
419
+ print('Bandwidth is %4d Hz/pixel' % (bwd) )
420
+ seq.set_definition("Bandwidth", f"{int(bwd)} Hz/px")
421
+ bdw_start = 1
422
+
423
+ idx_T = np.nonzero(torch.abs(rep.adc_usage))
424
+ dur = np.round(torch.sum(rep.event_time[idx_T],0).item(),decimals=5)
425
+
426
+ rise_time_x = 0
427
+ rise_time_y = 0
428
+ rise_time_z = 0
429
+
430
+ gx_gradmom = torch.sum(rep.gradm[idx_T,0]).item()*deltakx
431
+ if np.abs(gx_gradmom) > 0:
432
+ kwargs_for_gx = {"channel": 'x', "system": system, "flat_area": gx_gradmom, "flat_time": dur}
433
+ gx = make_trapezoid(**kwargs_for_gx)
434
+ rise_time_x = gx.rise_time
435
+
436
+ gy_gradmom = torch.sum(rep.gradm[idx_T,1]).item()*deltaky
437
+ if np.abs(gy_gradmom) > 0:
438
+ kwargs_for_gy = {"channel": 'y', "system": system, "flat_area": gy_gradmom, "flat_time": dur}
439
+ gy = make_trapezoid(**kwargs_for_gy)
440
+ rise_time_y = gy.rise_time
441
+
442
+ gz_gradmom = torch.sum(rep.gradm[idx_T,2]).item()*deltakz
443
+ if np.abs(gz_gradmom) > 0:
444
+ kwargs_for_gz = {"channel": 'z', "system": system, "flat_area": gz_gradmom, "flat_time": dur}
445
+ gz = make_trapezoid(**kwargs_for_gz)
446
+ rise_time_z = gz.rise_time
447
+
448
+ # calculate correct delay to have same starting point of flat top
449
+ shift = 0 # np.round(rep.event_time[idx_T[0]].item()/2,decimals=6) # heuristic delay, to be checked at scanner
450
+ x_delay = np.max([0,rise_time_y-rise_time_x,rise_time_z-rise_time_x])+shift
451
+ y_delay = np.max([0,rise_time_x-rise_time_y,rise_time_z-rise_time_y])+shift
452
+ z_delay = np.max([0,rise_time_x-rise_time_z,rise_time_y-rise_time_z])+shift
453
+
454
+ rise_time_x = 0
455
+ rise_time_y = 0
456
+ rise_time_z = 0
457
+
458
+ # adc gradient events are overwritten with correct delays
459
+ if np.abs(gx_gradmom) > 0:
460
+ kwargs_for_gx = {"channel": 'x', "system": system,"delay":x_delay, "flat_area": gx_gradmom, "flat_time": dur}
461
+ gx = make_trapezoid(**kwargs_for_gx)
462
+ rise_time_x = gx.rise_time
463
+ if np.abs(gy_gradmom) > 0:
464
+ kwargs_for_gy = {"channel": 'y', "system": system,"delay":y_delay, "flat_area": gy_gradmom, "flat_time": dur}
465
+ gy = make_trapezoid(**kwargs_for_gy)
466
+ rise_time_y = gy.rise_time
467
+ if np.abs(gz_gradmom) > 0:
468
+ kwargs_for_gz = {"channel": 'z', "system": system,"delay":z_delay, "flat_area": gz_gradmom, "flat_time": dur}
469
+ gz = make_trapezoid(**kwargs_for_gz)
470
+ rise_time_z = gz.rise_time
471
+
472
+ adc_delay = np.max([rise_time_x,rise_time_y,rise_time_z])+shift
473
+ kwargs_for_adc = {"num_samples": idx_T.size()[0], "duration": dur, "delay":(adc_delay), "phase_offset": rf_ex.phase_offset - np.pi/4}
474
+ adc = make_adc(**kwargs_for_adc)
475
+
476
+ # dont play zero grads (cant even do FID otherwise)
477
+ if rep.adc_usage[event] == -1:
478
+ print("Dummie ADC played out")
479
+ if np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom):
480
+ seq.add_block(gx,gy,gz)
481
+ elif np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0:
482
+ seq.add_block(gx,gy)
483
+ elif np.abs(gx_gradmom) > 0 and np.abs(gz_gradmom) > 0:
484
+ seq.add_block(gx,gz)
485
+ elif np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom) > 0:
486
+ seq.add_block(gy,gz)
487
+ elif np.abs(gx_gradmom) > 0:
488
+ seq.add_block(gx)
489
+ elif np.abs(gy_gradmom) > 0:
490
+ seq.add_block(gy)
491
+ elif np.abs(gz_gradmom) > 0:
492
+ seq.add_block(gz)
493
+ else:
494
+ if np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom):
495
+ seq.add_block(gx,gy,gz,adc)
496
+ elif np.abs(gx_gradmom) > 0 and np.abs(gy_gradmom) > 0:
497
+ seq.add_block(gx,gy,adc)
498
+ elif np.abs(gx_gradmom) > 0 and np.abs(gz_gradmom) > 0:
499
+ seq.add_block(gx,gz,adc)
500
+ elif np.abs(gy_gradmom) > 0 and np.abs(gz_gradmom) > 0:
501
+ seq.add_block(gy,gz,adc)
502
+ elif np.abs(gx_gradmom) > 0:
503
+ seq.add_block(gx,adc)
504
+ elif np.abs(gy_gradmom) > 0:
505
+ seq.add_block(gy,adc)
506
+ elif np.abs(gz_gradmom) > 0:
507
+ seq.add_block(gz,adc)
508
+ else:
509
+ seq.add_block(adc)
510
+
511
+ passes, report = seq.check_timing()
512
+ if not passes:
513
+ print("WARNING: Timing check failed:")
514
+ for line in report:
515
+ print(line, end="")
516
+
517
+ if plot_seq:
518
+ seq.plot()
519
+ seq.write(path)
520
+
521
+ append_header(path)
522
+
523
+ if write_data:
524
+ util.write_data_to_seq_file(seq_param, path)
525
+ print('Added kspace & adc_usage information to the .seq file!')
526
+
527
+ def append_header(path):
528
+ # append version and definitions
529
+ if sys.platform != 'linux':
530
+ try:
531
+ with open(r"\\141.67.249.47\MRTransfer\mrzero_src\.git\ORIG_HEAD") as file:
532
+ git_version = file.read()
533
+ except:
534
+ git_version = ''
535
+ with open(path, 'r') as fin:
536
+ lines = fin.read().splitlines(True)
537
+
538
+ updated_lines = []
539
+ updated_lines.append("# Pulseq sequence file\n")
540
+ updated_lines.append("# Created by MRIzero/IMR/GPI pulseq converter\n")
541
+ if sys.platform != 'linux':
542
+ updated_lines.append('# MRZero Version: 0.5, git hash: ' + git_version)
543
+ if sys.platform == 'linux':
544
+ updated_lines.append("# experiment_id: "+path.split('/')[-2]+"\n")
545
+ else:
546
+ updated_lines.append("# experiment_id: "+path.split('\\')[-2]+"\n")
547
+ updated_lines.append("# path: " + path + "\n")
548
+ updated_lines.append("\n")
549
+
550
+ updated_lines.extend(lines[3:])
551
+
552
+ with open(path, 'w') as fout:
553
+ fout.writelines(updated_lines)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+ from .pulseq_file import PulseqFile # noqa
3
+ from .pulse import Pulse
4
+ from .spoiler import Spoiler
5
+ from .adc import Adc
6
+
7
+
8
+ # ----- PULSES -----
9
+ # Simulated pulses are instantaneous. Pulseq blocks containing pulse events are
10
+ # split in the center of the pulse shape and converted into a gradient event
11
+ # before the pulse and a pulse + gradient event afterwards. This should result
12
+ # in a simulation that has no e.g. slice selection, but where the gradeint
13
+ # moments seen by refocussed, excited and unaffected magnetisation are correct.
14
+ # ----- ADC -----
15
+ # As stated in the specification, all samples are placed at (n + 0.5) * raster,
16
+ # which means when there is a gradient and an adc sample at the same time,
17
+ # the adc measurement sees only the first half of the gradient. TODO: check
18
+ # pulseq exporters and the Siemens interpreter to see if they respect the spec
19
+ # or if the adc samples should rather be placed at (n + 0 or 1) * raster.
20
+ # ----- GRADIENTS -----
21
+ # Only TRAP gradients are currently supported, arbitrary gradients would
22
+ # require to resample the gradient onto the ADC time grid. Note that the spec
23
+ # does not specify what should happen in between gradient samples, but there
24
+ # are two sensible options:
25
+ # - Mimic the scanner, where interpolation is given by the electronics, but a
26
+ # simple linear or bezier or similar interpolation should be a good choice
27
+ # - Adhere to the implementation of the official pulseq exporter, which
28
+ # probably assumes a piecewise constant gradient
29
+ # NOTE: As long as we don't simulate diffusion, we don't care about the shape
30
+ # of a gradient if it is not measured simultaneously.
31
+ # ----- Additional -----
32
+ # Simultaneous RF and ADC events are not supported but probably don't exist in
33
+ # pracitce anyways.
34
+
35
+
36
+ def intermediate(
37
+ file: PulseqFile
38
+ ) -> list[tuple[int, Pulse, list[Spoiler | Adc]]]:
39
+ seq = []
40
+ # Convert to intermediate representation
41
+ for block in file.blocks.values():
42
+ assert block.rf_id == 0 or block.adc_id == 0
43
+
44
+ if block.rf_id != 0:
45
+ seq += Pulse.parse(block, file)
46
+ elif block.adc_id != 0:
47
+ seq += Adc.parse(block, file)
48
+ else:
49
+ seq.append(Spoiler.parse(block, file))
50
+
51
+ reps = []
52
+ # [event_count, pulse, list of events]
53
+ current = [0, None, []] # Dummy for events before first pulse
54
+ # Split into repetitions
55
+ for block in seq:
56
+ if isinstance(block, Pulse):
57
+ current = [0, block, []]
58
+ reps.append(current)
59
+ else:
60
+ if isinstance(block, Adc):
61
+ current[0] += len(block.event_time)
62
+ else: # Spoiler
63
+ current[0] += 1
64
+ current[2].append(block)
65
+
66
+ return reps
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ from .pulseq_file import PulseqFile, Block
4
+ from .helpers import integrate
5
+ from .spoiler import Spoiler
6
+
7
+
8
+ class Adc:
9
+ def __init__(self, event_time: np.ndarray, gradm: np.ndarray, phase: float
10
+ ) -> None:
11
+ self.event_time = event_time
12
+ self.gradm = gradm
13
+ self.phase = phase
14
+
15
+ @classmethod
16
+ def parse(cls, block: Block, pulseq: PulseqFile) -> tuple[Adc, Spoiler]:
17
+ adc = pulseq.adcs[block.adc_id]
18
+ time = np.concatenate([
19
+ [0.0],
20
+ adc.delay + np.arange(adc.num) * adc.dwell,
21
+ [block.duration]
22
+ ])
23
+
24
+ gradm = np.zeros((adc.num + 1, 3))
25
+ if block.gx_id != 0:
26
+ grad = pulseq.grads[block.gx_id]
27
+ gradm[:, 0] = np.diff([integrate(grad, pulseq, t) for t in time])
28
+ if block.gy_id != 0:
29
+ grad = pulseq.grads[block.gy_id]
30
+ gradm[:, 1] = np.diff([integrate(grad, pulseq, t) for t in time])
31
+ if block.gz_id != 0:
32
+ grad = pulseq.grads[block.gz_id]
33
+ gradm[:, 2] = np.diff([integrate(grad, pulseq, t) for t in time])
34
+
35
+ event_time = np.diff(time)
36
+
37
+ fov = pulseq.definitions.fov
38
+ gradm[:, 0] *= fov[0]
39
+ gradm[:, 1] *= fov[1]
40
+ gradm[:, 2] *= fov[2]
41
+
42
+ return (
43
+ cls(event_time[:-1], gradm[:-1, :], adc.phase),
44
+ Spoiler(event_time[-1], gradm[-1, :])
45
+ )
46
+
47
+ def __repr__(self) -> str:
48
+ return f"ADC(event_time={self.event_time}, gradm={self.gradm})"