dsv2pulseq 0.0.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.
- dsv2pulseq/__init__.py +1 -0
- dsv2pulseq/dsv_to_pulseq.py +50 -0
- dsv2pulseq/helper.py +56 -0
- dsv2pulseq/insert_twix_data.py +62 -0
- dsv2pulseq/read_dsv.py +214 -0
- dsv2pulseq/read_dsv_inf.py +94 -0
- dsv2pulseq/read_dsv_samples.py +148 -0
- dsv2pulseq/sequence.py +517 -0
- dsv2pulseq-0.0.0.dist-info/METADATA +84 -0
- dsv2pulseq-0.0.0.dist-info/RECORD +14 -0
- dsv2pulseq-0.0.0.dist-info/WHEEL +5 -0
- dsv2pulseq-0.0.0.dist-info/entry_points.txt +3 -0
- dsv2pulseq-0.0.0.dist-info/licenses/LICENSE +21 -0
- dsv2pulseq-0.0.0.dist-info/top_level.txt +1 -0
dsv2pulseq/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .read_dsv import read_dsv, check_dsv
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from dsv2pulseq.read_dsv import read_dsv
|
|
7
|
+
|
|
8
|
+
defaults = {
|
|
9
|
+
'out_file': 'external.seq',
|
|
10
|
+
'ref_volt': 223.529007,
|
|
11
|
+
'lead_time': 100,
|
|
12
|
+
'hold_time': 30,
|
|
13
|
+
'adc_dead_time': 10,
|
|
14
|
+
'fov': [None, None, None],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def parse_arguments(argv=None):
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
description='Create Pulseq sequence file from dsv file.',
|
|
20
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument('in_file_prefix', type=str, help="Input dsv file prefix. E.g. 'gre'")
|
|
23
|
+
parser.add_argument('-o', '--out_file', type=str, help='Output Pulseq file.')
|
|
24
|
+
parser.add_argument('-r', '--ref_volt', type=float, help='Reference voltage of simulation [V].')
|
|
25
|
+
parser.add_argument('--lead_time', type=int, help='RF lead time [us].')
|
|
26
|
+
parser.add_argument('--hold_time', type=int, help='RF hold time [us].')
|
|
27
|
+
parser.add_argument('--adc_dead_time', type=int, help='ADC dead time [us].')
|
|
28
|
+
parser.add_argument('--fov', type=float, nargs=3, help='Field of view [mm] for x, y, z.')
|
|
29
|
+
parser.add_argument('--highgain', action='store_true', help='Set receiver gain to high.')
|
|
30
|
+
|
|
31
|
+
parser.set_defaults(**defaults)
|
|
32
|
+
return parser.parse_args(argv)
|
|
33
|
+
|
|
34
|
+
def main(argv=None):
|
|
35
|
+
args = parse_arguments(argv)
|
|
36
|
+
|
|
37
|
+
# check input
|
|
38
|
+
sfx = ['_INF', '_GRX', '_GRY', '_GRZ', '_RFD', '_RFP']
|
|
39
|
+
for suffix in sfx:
|
|
40
|
+
fname = args.in_file_prefix + suffix + '.dsv'
|
|
41
|
+
if not os.path.isfile(fname):
|
|
42
|
+
raise OSError(f"DSV file {fname} does not exist.")
|
|
43
|
+
|
|
44
|
+
seq = read_dsv(args.in_file_prefix, args.ref_volt, plot=False)
|
|
45
|
+
seq.set_lead_hold_time(args.lead_time, args.hold_time)
|
|
46
|
+
seq.set_adc_dead_time(args.adc_dead_time)
|
|
47
|
+
seq.make_pulseq_sequence(args.out_file, fov=args.fov, highgain=args.highgain)
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
sys.exit(main())
|
dsv2pulseq/helper.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper functions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
def trapezoid(amplitude, rise_time, flat_time, fall_time, dt=1e-5):
|
|
9
|
+
# Time segments
|
|
10
|
+
t_rise = np.arange(dt/2, rise_time, dt)
|
|
11
|
+
t_flat = np.arange(dt/2, flat_time, dt)
|
|
12
|
+
t_fall = np.arange(dt/2, fall_time, dt)
|
|
13
|
+
|
|
14
|
+
# Signal segments
|
|
15
|
+
rise = (amplitude / rise_time) * t_rise
|
|
16
|
+
plateau = np.ones_like(t_flat) * amplitude
|
|
17
|
+
fall = amplitude - (amplitude / fall_time) * t_fall
|
|
18
|
+
|
|
19
|
+
# Concatenate full waveform
|
|
20
|
+
waveform = np.concatenate((rise, plateau, fall))
|
|
21
|
+
|
|
22
|
+
return waveform
|
|
23
|
+
|
|
24
|
+
def waveform_from_seqblock(grad, system=None):
|
|
25
|
+
"""
|
|
26
|
+
extracts gradient waveform from Pypulseq sequence block
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
if grad.type == 'trap':
|
|
30
|
+
waveform = trapezoid(grad.amplitude, grad.rise_time, grad.flat_time, grad.fall_time, dt=system.grad_raster_time)
|
|
31
|
+
else:
|
|
32
|
+
waveform = grad.waveform
|
|
33
|
+
|
|
34
|
+
waveform = np.concatenate((np.zeros(int(grad.delay / system.grad_raster_time)), waveform))
|
|
35
|
+
|
|
36
|
+
return waveform
|
|
37
|
+
|
|
38
|
+
def round_up_to_raster(number, decimals=5, tol=1e-10):
|
|
39
|
+
"""
|
|
40
|
+
Round number up to a specific number of decimal places.
|
|
41
|
+
Rounds up only if the digit beyond the desired precision exceeds a tolerance.
|
|
42
|
+
This avoids rounding up for tiny floating point errors.
|
|
43
|
+
|
|
44
|
+
Parameters:
|
|
45
|
+
- number: float, the value to round
|
|
46
|
+
- decimals: int, number of decimal places
|
|
47
|
+
- tol: float, minimum excess to consider as real (not floating point noise)
|
|
48
|
+
"""
|
|
49
|
+
multiplier = 10 ** decimals
|
|
50
|
+
scaled = number * multiplier
|
|
51
|
+
rounded = math.floor(scaled)
|
|
52
|
+
|
|
53
|
+
if scaled - rounded > tol:
|
|
54
|
+
rounded += 1
|
|
55
|
+
|
|
56
|
+
return rounded / multiplier
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import twixtools
|
|
5
|
+
import ismrmrd
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
defaults = {'out_file': 'merged_rawdata.dat'}
|
|
9
|
+
|
|
10
|
+
def parse_arguments(argv=None):
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
description='Merge Pulseq data into Siemens raw data file.',
|
|
13
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument('in_file_1', type=str, help="Siemens raw data file acquired with original sequence.")
|
|
16
|
+
parser.add_argument('in_file_2', type=str, help="Pulseq raw data file.")
|
|
17
|
+
parser.add_argument('-o', '--out_file', type=str, help='Output Siemens raw data file name.')
|
|
18
|
+
|
|
19
|
+
parser.set_defaults(**defaults)
|
|
20
|
+
return parser.parse_args(argv)
|
|
21
|
+
|
|
22
|
+
def main(argv=None):
|
|
23
|
+
args = parse_arguments(argv)
|
|
24
|
+
|
|
25
|
+
if not args.in_file_1.endswith('.dat'):
|
|
26
|
+
raise ValueError("Input file 1 must be a Siemens raw data file '.dat'.")
|
|
27
|
+
if not (args.in_file_2.endswith('.dat') or args.in_file_2.endswith('.mrd')):
|
|
28
|
+
raise ValueError("Input file 2 (Pulseq data file) must be either a Siemens raw data file '.dat' or ISMRMRD file '.mrd'.")
|
|
29
|
+
use_mrd = args.in_file_2.endswith('.mrd')
|
|
30
|
+
|
|
31
|
+
file1 = twixtools.read_twix(args.in_file_1, keep_syncdata=True, keep_acqend=True)
|
|
32
|
+
n_acq1 = len(file1[-1]['mdb'])
|
|
33
|
+
if use_mrd:
|
|
34
|
+
file2 = ismrmrd.Dataset(args.in_file_2)
|
|
35
|
+
n_acq2 = file2.number_of_acquisitions()
|
|
36
|
+
if n_acq1 - n_acq2 == 1:
|
|
37
|
+
n_acq2 += 1 # ACQEND is usually not converted to MRD
|
|
38
|
+
else:
|
|
39
|
+
file2 = twixtools.read_twix(args.in_file_2, keep_syncdata=True, keep_acqend=True)
|
|
40
|
+
n_acq2 = len(file2[-1]['mdb'])
|
|
41
|
+
|
|
42
|
+
if n_acq1 != n_acq2:
|
|
43
|
+
raise ValueError(f"Files have different number of measurement data blocks. File 1: {n_acq1}, File 2: {n_acq2}.")
|
|
44
|
+
|
|
45
|
+
print(f"Read and copy {n_acq1} measurement data blocks.")
|
|
46
|
+
for k, mdb in enumerate(file1[-1]['mdb']):
|
|
47
|
+
if not mdb.is_flag_set('ACQEND') and not mdb.is_flag_set('SYNCDATA'):
|
|
48
|
+
mdb = mdb.convert_to_local()
|
|
49
|
+
if use_mrd:
|
|
50
|
+
mdb.data = file2.read_acquisition(k).data.copy()
|
|
51
|
+
else:
|
|
52
|
+
mdb.data = file2[-1]['mdb'][k].data.copy()
|
|
53
|
+
file1[-1]['mdb'][k] = mdb
|
|
54
|
+
|
|
55
|
+
if use_mrd:
|
|
56
|
+
file2.close()
|
|
57
|
+
|
|
58
|
+
print(f"Writing merged data to {args.out_file}.")
|
|
59
|
+
twixtools.write_twix(file1, args.out_file, version_is_ve=True)
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
sys.exit(main())
|
dsv2pulseq/read_dsv.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import numpy as np
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from dsv2pulseq.sequence import Sequence
|
|
6
|
+
from dsv2pulseq.read_dsv_samples import DSVFile
|
|
7
|
+
from dsv2pulseq.read_dsv_inf import read_dsv_inf
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.INFO)
|
|
10
|
+
|
|
11
|
+
def plot_seq(dsv):
|
|
12
|
+
"""
|
|
13
|
+
Plots the complete sequence
|
|
14
|
+
"""
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
|
|
17
|
+
plt.figure(figsize=(20,20))
|
|
18
|
+
plt.subplot(511)
|
|
19
|
+
dsv[0].plot()
|
|
20
|
+
plt.subplot(512)
|
|
21
|
+
dsv[1].plot()
|
|
22
|
+
plt.subplot(513)
|
|
23
|
+
dsv[2].plot()
|
|
24
|
+
plt.subplot(514)
|
|
25
|
+
dsv[3].plot()
|
|
26
|
+
plt.subplot(515)
|
|
27
|
+
dsv[4].plot()
|
|
28
|
+
|
|
29
|
+
def read_dsv(file_prefix, ref_volt=223.529007, plot=False):
|
|
30
|
+
"""
|
|
31
|
+
Reads dsv files and returns complete sequence
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
file_prefix: dsv file prefix, e.g. 'gre'
|
|
35
|
+
ref_volt: reference voltage of the simulation in V
|
|
36
|
+
plot: if True, plots the sequence shapes
|
|
37
|
+
Returns:
|
|
38
|
+
seq: Sequence object containing the shapes and blocks
|
|
39
|
+
"""
|
|
40
|
+
seq = Sequence(ref_volt)
|
|
41
|
+
|
|
42
|
+
# read event shapes
|
|
43
|
+
logging.info(f"Read RF and gradient shapes of {file_prefix} sequence.")
|
|
44
|
+
start_dsv = time.time()
|
|
45
|
+
rfd = DSVFile(file_prefix+"_RFD.dsv")
|
|
46
|
+
rfp = DSVFile(file_prefix+"_RFP.dsv")
|
|
47
|
+
grx = DSVFile(file_prefix+"_GRX.dsv")
|
|
48
|
+
gry = DSVFile(file_prefix+"_GRY.dsv")
|
|
49
|
+
grz = DSVFile(file_prefix+"_GRZ.dsv")
|
|
50
|
+
end_dsv = time.time()
|
|
51
|
+
logging.info(f"Finished reading dsv files in {(end_dsv-start_dsv):.2f}s.")
|
|
52
|
+
|
|
53
|
+
shapes = [rfd, rfp, grx, gry, grz]
|
|
54
|
+
seq.set_shapes(shapes)
|
|
55
|
+
|
|
56
|
+
# Read block structure
|
|
57
|
+
logging.info("Read block structure.")
|
|
58
|
+
start_inf = time.time()
|
|
59
|
+
read_dsv_inf(file_prefix+"_INF.dsv", seq)
|
|
60
|
+
end_inf = time.time()
|
|
61
|
+
logging.info(f"Finished reading block structure in {(end_inf-start_inf):.2f}s.")
|
|
62
|
+
|
|
63
|
+
if plot:
|
|
64
|
+
plot_seq([rfd,rfp,grx,gry,grz])
|
|
65
|
+
|
|
66
|
+
return seq
|
|
67
|
+
|
|
68
|
+
def check_dsv(file_prefix1, file_prefix2, time_shift=20):
|
|
69
|
+
"""
|
|
70
|
+
Checks, if dsv files from converted sequence are the same as in the original sequence
|
|
71
|
+
|
|
72
|
+
Pulseq sequence shape usually have two 20us long additional global
|
|
73
|
+
freq/phase event in the beginning and end of the sequence. This can be
|
|
74
|
+
compensated by the time_shift parameter.
|
|
75
|
+
|
|
76
|
+
If provided, also the ADC shapes are compared.
|
|
77
|
+
|
|
78
|
+
Parameters:
|
|
79
|
+
file_prefix1: dsv file prefix of the original sequence
|
|
80
|
+
file_prefix2: dsv file prefix of the Pulseq sequence
|
|
81
|
+
time_shift: time shift in us for the Pulseq sequence
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
seq1: Sequence object of the original sequence
|
|
85
|
+
seq2: Sequence object of the Pulseq sequence
|
|
86
|
+
"""
|
|
87
|
+
import matplotlib.pyplot as plt
|
|
88
|
+
|
|
89
|
+
seq1 = read_dsv(file_prefix1)
|
|
90
|
+
seq2 = read_dsv(file_prefix2)
|
|
91
|
+
|
|
92
|
+
if os.path.exists(file_prefix1 + "_ADC.dsv") and os.path.exists(file_prefix2 + "_ADC.dsv"):
|
|
93
|
+
adc1_dsv = DSVFile(file_prefix1 + "_ADC.dsv")
|
|
94
|
+
adc2_dsv = DSVFile(file_prefix2 + "_ADC.dsv")
|
|
95
|
+
adc_delta = adc1_dsv.definitions.horidelta
|
|
96
|
+
seq1.adc = adc1_dsv.values
|
|
97
|
+
seq2.adc = adc2_dsv.values
|
|
98
|
+
else:
|
|
99
|
+
seq1.adc = None
|
|
100
|
+
seq2.adc = None
|
|
101
|
+
|
|
102
|
+
rfd1 = abs(seq1.rf)
|
|
103
|
+
rfp1 = np.angle(seq1.rf)
|
|
104
|
+
grx1 = seq1.gx
|
|
105
|
+
gry1 = seq1.gy
|
|
106
|
+
grz1 = seq1.gz
|
|
107
|
+
adc1 = seq1.adc
|
|
108
|
+
|
|
109
|
+
rfd2 = abs(seq2.rf)
|
|
110
|
+
rfp2 = np.angle(seq2.rf)
|
|
111
|
+
grx2 = seq2.gx
|
|
112
|
+
gry2 = seq2.gy
|
|
113
|
+
grz2 = seq2.gz
|
|
114
|
+
adc2 = seq2.adc
|
|
115
|
+
|
|
116
|
+
shift_rf = int(time_shift / seq1.delta_rf)
|
|
117
|
+
shift_grad = int(time_shift / seq1.delta_grad)
|
|
118
|
+
|
|
119
|
+
rfd2 = rfd2[shift_rf:-shift_rf]
|
|
120
|
+
rfp2 = rfp2[shift_rf:-shift_rf]
|
|
121
|
+
grx2 = grx2[shift_grad:-shift_grad]
|
|
122
|
+
gry2 = gry2[shift_grad:-shift_grad]
|
|
123
|
+
grz2 = grz2[shift_grad:-shift_grad]
|
|
124
|
+
if adc1 is not None and adc2 is not None:
|
|
125
|
+
shift_adc = int(time_shift / adc_delta)
|
|
126
|
+
adc2 = adc2[shift_adc:-shift_adc]
|
|
127
|
+
subplots = 6
|
|
128
|
+
else:
|
|
129
|
+
subplots = 5
|
|
130
|
+
|
|
131
|
+
len_rf = min(len(rfd1), len(rfd2))
|
|
132
|
+
len_gx = min(len(grx1), len(grx2))
|
|
133
|
+
len_gy = min(len(gry1), len(gry2))
|
|
134
|
+
len_gz = min(len(grz1), len(grz2))
|
|
135
|
+
if adc1 is not None and adc2 is not None:
|
|
136
|
+
len_adc = min(len(adc1), len(adc2))
|
|
137
|
+
|
|
138
|
+
# Plot sequence 1
|
|
139
|
+
n_subplots = subplots # 5 or 6
|
|
140
|
+
plt.figure(figsize=(10, 2 * n_subplots))
|
|
141
|
+
plt.suptitle(f"Sequence 1: {file_prefix1}", fontsize=16)
|
|
142
|
+
plt.subplot(n_subplots, 1, 1)
|
|
143
|
+
plt.plot(rfd1)
|
|
144
|
+
plt.title("RFD")
|
|
145
|
+
plt.subplot(n_subplots, 1, 2)
|
|
146
|
+
plt.plot(rfp1)
|
|
147
|
+
plt.title("RFP")
|
|
148
|
+
plt.subplot(n_subplots, 1, 3)
|
|
149
|
+
plt.plot(grx1)
|
|
150
|
+
plt.title("GX")
|
|
151
|
+
plt.subplot(n_subplots, 1, 4)
|
|
152
|
+
plt.plot(gry1)
|
|
153
|
+
plt.title("GY")
|
|
154
|
+
plt.subplot(n_subplots, 1, 5)
|
|
155
|
+
plt.plot(grz1)
|
|
156
|
+
plt.title("GZ")
|
|
157
|
+
if n_subplots == 6:
|
|
158
|
+
plt.subplot(n_subplots, 1, 6)
|
|
159
|
+
plt.plot(adc1)
|
|
160
|
+
plt.title("ADC")
|
|
161
|
+
plt.tight_layout(rect=[0, 0, 1, 0.96])
|
|
162
|
+
|
|
163
|
+
# Plot sequence 2
|
|
164
|
+
plt.figure(figsize=(10, 2 * n_subplots))
|
|
165
|
+
plt.suptitle(f"Sequence 2: {file_prefix2}", fontsize=16)
|
|
166
|
+
plt.subplot(n_subplots, 1, 1)
|
|
167
|
+
plt.plot(rfd2)
|
|
168
|
+
plt.title("RFD")
|
|
169
|
+
plt.subplot(n_subplots, 1, 2)
|
|
170
|
+
plt.plot(rfp2)
|
|
171
|
+
plt.title("RFP")
|
|
172
|
+
plt.subplot(n_subplots, 1, 3)
|
|
173
|
+
plt.plot(grx2)
|
|
174
|
+
plt.title("GX")
|
|
175
|
+
plt.subplot(n_subplots, 1, 4)
|
|
176
|
+
plt.plot(gry2)
|
|
177
|
+
plt.title("GY")
|
|
178
|
+
plt.subplot(n_subplots, 1, 5)
|
|
179
|
+
plt.plot(grz2)
|
|
180
|
+
plt.title("GZ")
|
|
181
|
+
if n_subplots == 6:
|
|
182
|
+
plt.subplot(n_subplots, 1, 6)
|
|
183
|
+
plt.plot(adc2)
|
|
184
|
+
plt.title("ADC")
|
|
185
|
+
plt.tight_layout(rect=[0, 0, 1, 0.96])
|
|
186
|
+
|
|
187
|
+
# Plot difference
|
|
188
|
+
plt.figure(figsize=(10, 2 * n_subplots))
|
|
189
|
+
plt.suptitle(f"Difference: {file_prefix1} - {file_prefix2}", fontsize=16)
|
|
190
|
+
plt.subplot(n_subplots, 1, 1)
|
|
191
|
+
plt.plot(rfd1[:len_rf] - rfd2[:len_rf])
|
|
192
|
+
plt.title("RFD")
|
|
193
|
+
plt.subplot(n_subplots, 1, 2)
|
|
194
|
+
plt.plot(rfp1[:len_rf] - rfp2[:len_rf])
|
|
195
|
+
plt.title("RFP")
|
|
196
|
+
plt.subplot(n_subplots, 1, 3)
|
|
197
|
+
plt.plot(grx1[:len_gx] - grx2[:len_gx])
|
|
198
|
+
plt.title("GX")
|
|
199
|
+
plt.subplot(n_subplots, 1, 4)
|
|
200
|
+
plt.plot(gry1[:len_gy] - gry2[:len_gy])
|
|
201
|
+
plt.title("GY")
|
|
202
|
+
plt.subplot(n_subplots, 1, 5)
|
|
203
|
+
plt.plot(grz1[:len_gz] - grz2[:len_gz])
|
|
204
|
+
plt.title("GZ")
|
|
205
|
+
if n_subplots == 6:
|
|
206
|
+
plt.subplot(n_subplots, 1, 6)
|
|
207
|
+
plt.plot(adc1[:len_adc] - adc2[:len_adc])
|
|
208
|
+
plt.title("ADC")
|
|
209
|
+
plt.tight_layout(rect=[0, 0, 1, 0.96])
|
|
210
|
+
|
|
211
|
+
plt.show()
|
|
212
|
+
|
|
213
|
+
return seq1, seq2
|
|
214
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read a dsv INF file and append blocks to sequence
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
def find_char(s, ch):
|
|
6
|
+
return [i for i, ltr in enumerate(s) if ltr == ch]
|
|
7
|
+
|
|
8
|
+
def read_dsv_inf(file, seq):
|
|
9
|
+
""" Read INF dsv file (tested only for VE)
|
|
10
|
+
|
|
11
|
+
Important: The INF file contains the gradients in logical coordinate system (GP,GR,GS)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
delta_rf = seq.delta_rf
|
|
15
|
+
delta_grad = seq.delta_grad
|
|
16
|
+
|
|
17
|
+
with open(file, 'r') as f:
|
|
18
|
+
line_ix = float('inf')
|
|
19
|
+
block_idx = -1
|
|
20
|
+
for k,line in enumerate(f):
|
|
21
|
+
if 'EventBlock' in line:
|
|
22
|
+
block_idx = seq.n_blocks
|
|
23
|
+
char_ix = find_char(line, ' ')
|
|
24
|
+
start_time = int(line[char_ix[-2]:char_ix[-1]])
|
|
25
|
+
duration = int(line[char_ix[-1]:])
|
|
26
|
+
seq.add_block(block_idx, duration, start_time)
|
|
27
|
+
if 'Rel.Time' in line:
|
|
28
|
+
ix = find_char(line, '|') # position of events in line
|
|
29
|
+
line_ix = k+2
|
|
30
|
+
ts_old = 0
|
|
31
|
+
freq_phase = None
|
|
32
|
+
if '|' not in line:
|
|
33
|
+
# end of event block
|
|
34
|
+
line_ix = float('inf')
|
|
35
|
+
ts = -1
|
|
36
|
+
if k>=line_ix:
|
|
37
|
+
block = seq.get_block(block_idx)
|
|
38
|
+
ts = int(line[ix[0]+1:ix[1]].strip())
|
|
39
|
+
block.add_timestamp(ts)
|
|
40
|
+
|
|
41
|
+
if ts != ts_old and freq_phase is not None:
|
|
42
|
+
block.set_freqphase(freq_phase, ts_old)
|
|
43
|
+
freq_phase = None
|
|
44
|
+
|
|
45
|
+
if line[ix[1]+1:ix[2]].strip():
|
|
46
|
+
rf_str = line[ix[1]+1:ix[2]].strip()
|
|
47
|
+
rf_dur = int(rf_str[rf_str.rfind('/')+1:].strip())
|
|
48
|
+
rf_shape_ix = slice((block.start_time+ts)//delta_rf, (block.start_time+ts+rf_dur)//delta_rf)
|
|
49
|
+
block.add_rf(rf_dur, rf_shape_ix, ts)
|
|
50
|
+
if line[ix[2]+1:ix[3]].strip():
|
|
51
|
+
freqphase_str = line[ix[2]+1:ix[3]].strip()
|
|
52
|
+
freq = float(freqphase_str[freqphase_str.rfind(':')+1:freqphase_str.rfind('/')].strip())
|
|
53
|
+
phase = float(freqphase_str[freqphase_str.rfind('/')+1:].strip())
|
|
54
|
+
freq_phase = [freq, phase]
|
|
55
|
+
if line[ix[4]+1:ix[5]].strip():
|
|
56
|
+
adc_str = line[ix[4]+1:ix[5]].strip()
|
|
57
|
+
adc_samples = float(adc_str[adc_str.rfind(':')+1:adc_str.rfind('/')].strip())
|
|
58
|
+
adc_dur = float(adc_str[adc_str.rfind('/')+1:].strip())
|
|
59
|
+
if "VOP_READOUT" not in adc_str: # pTx RX events during RFs are not ADCs
|
|
60
|
+
block.add_adc(adc_dur, adc_samples, ts)
|
|
61
|
+
if line[ix[5]+1:ix[6]].strip():
|
|
62
|
+
gp_str = line[ix[5]+1:ix[6]].strip()
|
|
63
|
+
gp_ix = find_char(gp_str, '/')
|
|
64
|
+
gp_amp = float(gp_str[gp_str.rfind(':')+1:gp_ix[0]].strip()) # amplitude in logical! coordinate system
|
|
65
|
+
gp_rut = int(gp_str[gp_ix[0]+1:gp_ix[1]].strip())
|
|
66
|
+
gp_dur = int(gp_str[gp_ix[1]+1:gp_ix[2]].strip())
|
|
67
|
+
gp_rdt = int(gp_str[gp_ix[2]+1:].strip())
|
|
68
|
+
gp_shape_ix = slice((block.start_time+ts)//delta_grad, (block.start_time+ts+gp_dur+gp_rdt)//delta_grad)
|
|
69
|
+
block.add_grad('p', gp_amp, gp_dur, gp_rut, gp_rdt, gp_shape_ix, ts)
|
|
70
|
+
if line[ix[6]+1:ix[7]].strip():
|
|
71
|
+
gr_str = line[ix[6]+1:ix[7]].strip()
|
|
72
|
+
gr_ix = find_char(gr_str, '/')
|
|
73
|
+
gr_amp = float(gr_str[gr_str.rfind(':')+1:gr_ix[0]].strip())
|
|
74
|
+
gr_rut = int(gr_str[gr_ix[0]+1:gr_ix[1]].strip())
|
|
75
|
+
gr_dur = int(gr_str[gr_ix[1]+1:gr_ix[2]].strip())
|
|
76
|
+
gr_rdt = int(gr_str[gr_ix[2]+1:].strip())
|
|
77
|
+
gr_shape_ix = slice((block.start_time+ts)//delta_grad, (block.start_time+ts+gr_dur+gr_rdt)//delta_grad)
|
|
78
|
+
block.add_grad('r', gr_amp, gr_dur, gr_rut, gr_rdt, gr_shape_ix, ts)
|
|
79
|
+
if line[ix[7]+1:ix[8]].strip():
|
|
80
|
+
gs_str = line[ix[7]+1:ix[8]].strip()
|
|
81
|
+
gs_ix = find_char(gs_str, '/')
|
|
82
|
+
gs_amp = float(gs_str[gs_str.rfind(':')+1:gs_ix[0]].strip())
|
|
83
|
+
gs_rut = int(gs_str[gs_ix[0]+1:gs_ix[1]].strip())
|
|
84
|
+
gs_dur = int(gs_str[gs_ix[1]+1:gs_ix[2]].strip())
|
|
85
|
+
gs_rdt = int(gs_str[gs_ix[2]+1:].strip())
|
|
86
|
+
gs_shape_ix = slice((block.start_time+ts)//delta_grad, (block.start_time+ts+gs_dur+gs_rdt)//delta_grad)
|
|
87
|
+
block.add_grad('s', gs_amp, gs_dur, gs_rut, gs_rdt, gs_shape_ix, ts)
|
|
88
|
+
if line[ix[8]+1:ix[9]].strip():
|
|
89
|
+
trig_str = line[ix[8]+1:ix[9]].strip()
|
|
90
|
+
trig_type = trig_str[trig_str.rfind(':')+1:trig_str.rfind('/')].strip()
|
|
91
|
+
trig_dur = int(trig_str[trig_str.rfind('/')+1:].strip())
|
|
92
|
+
block.add_trig(trig_dur, trig_type, ts)
|
|
93
|
+
|
|
94
|
+
ts_old = ts
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This code is copied from the dsvreader from Markus Boland.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# coding=utf-8
|
|
6
|
+
import numpy as np
|
|
7
|
+
import codecs
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
from warnings import warn
|
|
10
|
+
|
|
11
|
+
class DSVFile:
|
|
12
|
+
"""
|
|
13
|
+
This class loads and uncompresses a dsv file.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __decode_dsv_values(self):
|
|
17
|
+
"""
|
|
18
|
+
Decodes a compressed dsv array
|
|
19
|
+
:return: Decompressed dsv array
|
|
20
|
+
|
|
21
|
+
The values are signed 32-bit integers. The real value
|
|
22
|
+
will be obtained after division by VERTFACTOR.
|
|
23
|
+
If the same sample value is repeated several times,
|
|
24
|
+
the value is only listed twice, and the third value
|
|
25
|
+
specifies the number of repetitions.
|
|
26
|
+
Only the first sample is stored as an absolute value.
|
|
27
|
+
All other values are deltas to the preceeding value.
|
|
28
|
+
(This way it is possible to compress linear slopes.)
|
|
29
|
+
"""
|
|
30
|
+
ndsv = np.zeros((int(self.definitions.samples)), dtype=int)
|
|
31
|
+
dsv = np.array(self.values, dtype=int)
|
|
32
|
+
count = 0
|
|
33
|
+
ncount = 1
|
|
34
|
+
ndsv[ncount] = dsv[count]
|
|
35
|
+
prev = dsv[0]
|
|
36
|
+
while count < dsv.shape[0]-1:
|
|
37
|
+
count += 1
|
|
38
|
+
if not (dsv[count] == prev): # no repetition
|
|
39
|
+
ndsv[ncount] = ndsv[ncount-1] + dsv[count]
|
|
40
|
+
prev = dsv[count]
|
|
41
|
+
ncount += 1
|
|
42
|
+
else: # number repeated
|
|
43
|
+
# get repetitions
|
|
44
|
+
nrep = dsv[count+1]
|
|
45
|
+
ndsv[ncount] = ndsv[ncount-1] + dsv[count]
|
|
46
|
+
ncount += 1
|
|
47
|
+
for i in range(nrep):
|
|
48
|
+
ndsv[ncount] = ndsv[ncount-1] + dsv[count]
|
|
49
|
+
ncount += 1
|
|
50
|
+
# store last value
|
|
51
|
+
prev = dsv[count]
|
|
52
|
+
|
|
53
|
+
# inc counters
|
|
54
|
+
count += 1
|
|
55
|
+
|
|
56
|
+
# apply vertical factor
|
|
57
|
+
ndsv = ndsv / self.definitions.vertfactor
|
|
58
|
+
return ndsv
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def __isfloat(value):
|
|
62
|
+
"""
|
|
63
|
+
Returns if string contains a valid float
|
|
64
|
+
:param value: String
|
|
65
|
+
:return: Float?
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
float(value)
|
|
69
|
+
return True
|
|
70
|
+
except ValueError:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def __init__(self, filename, encoding='cp1252'):
|
|
74
|
+
"""
|
|
75
|
+
Loads the data from filename
|
|
76
|
+
:param filename: Path to dsv file
|
|
77
|
+
:param encoding: Encoding of dsv file
|
|
78
|
+
:return:
|
|
79
|
+
The values are automatically uncompressed and stored in values field. Other parameters
|
|
80
|
+
are stored in field given by the structure of the dsv file. The values are converted
|
|
81
|
+
to float if possible and remain unicode if not (should be right for all params).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# load file
|
|
85
|
+
dsv_file = codecs.open(filename, 'r', encoding)
|
|
86
|
+
try:
|
|
87
|
+
filecnt = dsv_file.readlines()
|
|
88
|
+
finally:
|
|
89
|
+
dsv_file.close()
|
|
90
|
+
|
|
91
|
+
# trim lines
|
|
92
|
+
filecnt = list(map(str.strip, filecnt))
|
|
93
|
+
filecnt = [x for x in filecnt if len(x) > 0]
|
|
94
|
+
filecnt = [x for x in filecnt if not x.startswith(';')]
|
|
95
|
+
filecnt = list(map(str.lower, filecnt))
|
|
96
|
+
|
|
97
|
+
file_data = dict()
|
|
98
|
+
dictkeys = {'definitions', 'filetype', 'frame'}
|
|
99
|
+
listkeys = {'values'}
|
|
100
|
+
curkey = ''
|
|
101
|
+
for line in filecnt:
|
|
102
|
+
# Section line
|
|
103
|
+
if line.startswith('[') and line.endswith(']'):
|
|
104
|
+
line = line[1:-1]
|
|
105
|
+
if line in dictkeys:
|
|
106
|
+
file_data[line] = dict()
|
|
107
|
+
curkey = line
|
|
108
|
+
elif line in listkeys:
|
|
109
|
+
setattr(self, line, list())
|
|
110
|
+
curkey = line
|
|
111
|
+
else:
|
|
112
|
+
warn(f'Unknown key {line} in DSV file {filename}... skipping.')
|
|
113
|
+
# Key value pair
|
|
114
|
+
elif line.find('=') > 0:
|
|
115
|
+
key, val = line.split('=')
|
|
116
|
+
if self.__isfloat(val):
|
|
117
|
+
val = float(val)
|
|
118
|
+
file_data[curkey][key] = val
|
|
119
|
+
elif curkey in listkeys:
|
|
120
|
+
getattr(self, curkey).append(int(line))
|
|
121
|
+
|
|
122
|
+
# set params
|
|
123
|
+
for _d in dictkeys:
|
|
124
|
+
setattr(self, _d, namedtuple(_d, list(file_data[_d].keys()))(**file_data[_d]))
|
|
125
|
+
|
|
126
|
+
# uncompress values
|
|
127
|
+
self.values = self.__decode_dsv_values()
|
|
128
|
+
self.time = np.arange(self.values.shape[0]) * self.definitions.horidelta
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def plot(self, smin=0, skip=1000, smax=1000000):
|
|
132
|
+
"""
|
|
133
|
+
Plots the stored dsv data
|
|
134
|
+
:param smin: Start index to plot
|
|
135
|
+
:param skip: Index delta
|
|
136
|
+
:param smax: Stop index
|
|
137
|
+
:return:
|
|
138
|
+
"""
|
|
139
|
+
import matplotlib.pyplot as plt
|
|
140
|
+
|
|
141
|
+
smin *= int(10./self.definitions.horidelta)
|
|
142
|
+
smax *= int(10./self.definitions.horidelta)
|
|
143
|
+
skip *= int(10./self.definitions.horidelta)
|
|
144
|
+
|
|
145
|
+
plt.plot(self.time[smin:smax:skip], self.values[smin:smax:skip])
|
|
146
|
+
plt.xlabel(self.definitions.horiunitname)
|
|
147
|
+
plt.ylabel(self.definitions.vertunitname)
|
|
148
|
+
pass
|
dsv2pulseq/sequence.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import copy
|
|
5
|
+
from warnings import warn
|
|
6
|
+
import logging
|
|
7
|
+
import pypulseq as pp
|
|
8
|
+
from dsv2pulseq.helper import round_up_to_raster, waveform_from_seqblock
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Block():
|
|
12
|
+
"""
|
|
13
|
+
Event block in "Siemens style", allows more than one object per channel
|
|
14
|
+
All units are in Siemens units:
|
|
15
|
+
times: [us]
|
|
16
|
+
gradients: [mT/m]
|
|
17
|
+
RF pulses: [V]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, idx, duration, start_time):
|
|
21
|
+
self.block_idx = idx
|
|
22
|
+
self.block_duration = duration
|
|
23
|
+
self.start_time = start_time
|
|
24
|
+
self.timestamps = {}
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return f"Block {self.block_idx} with duration {self.block_duration} us at start time {self.start_time} us with timestamps {self.timestamps}."
|
|
28
|
+
|
|
29
|
+
def add_timestamp(self, ts):
|
|
30
|
+
if not ts in self.timestamps:
|
|
31
|
+
self.timestamps[ts] = []
|
|
32
|
+
|
|
33
|
+
def add_rf(self, duration, shape_ix, ts):
|
|
34
|
+
self.add_timestamp(ts)
|
|
35
|
+
rf = Rf(duration, shape_ix)
|
|
36
|
+
self.timestamps[ts].append(rf)
|
|
37
|
+
|
|
38
|
+
def add_grad(self, channel, amp, duration, ramp_up, ramp_dn, shape_ix, ts):
|
|
39
|
+
self.add_timestamp(ts)
|
|
40
|
+
grad = Grad(channel, amp, duration, ramp_up, ramp_dn, shape_ix)
|
|
41
|
+
self.timestamps[ts].append(grad)
|
|
42
|
+
|
|
43
|
+
def add_adc(self, duration, samples, ts):
|
|
44
|
+
self.add_timestamp(ts)
|
|
45
|
+
adc = Adc(duration, samples)
|
|
46
|
+
self.timestamps[ts].append(adc)
|
|
47
|
+
|
|
48
|
+
def add_trig(self, duration, trig_type, ts):
|
|
49
|
+
self.add_timestamp(ts)
|
|
50
|
+
trig = Trig(duration, trig_type)
|
|
51
|
+
self.timestamps[ts].append(trig)
|
|
52
|
+
|
|
53
|
+
def set_freqphase(self, freq_phase, ts):
|
|
54
|
+
for event in self.timestamps[ts]:
|
|
55
|
+
if event.type == 'rf' or event.type == 'adc':
|
|
56
|
+
event.set_freqphase(freq_phase)
|
|
57
|
+
|
|
58
|
+
class Rf():
|
|
59
|
+
|
|
60
|
+
def __init__(self, duration, shape_ix):
|
|
61
|
+
self.type = 'rf'
|
|
62
|
+
self.duration = duration
|
|
63
|
+
self.freq = 0
|
|
64
|
+
self.phase = 0
|
|
65
|
+
self.shape_ix = shape_ix
|
|
66
|
+
|
|
67
|
+
def set_freqphase(self, freq_phase):
|
|
68
|
+
self.freq = freq_phase[0]
|
|
69
|
+
self.phase = freq_phase[1]
|
|
70
|
+
|
|
71
|
+
class Grad():
|
|
72
|
+
|
|
73
|
+
def __init__(self, channel, amp, duration, ramp_up, ramp_dn, shape_ix):
|
|
74
|
+
self.type = 'g' + channel
|
|
75
|
+
self.channel = channel
|
|
76
|
+
self.amp = amp # amplitude in logical! coordinate system
|
|
77
|
+
self.duration = duration # flat_top + ramp_up
|
|
78
|
+
self.ramp_up = ramp_up
|
|
79
|
+
self.ramp_dn = ramp_dn
|
|
80
|
+
self.shape_ix = shape_ix
|
|
81
|
+
|
|
82
|
+
class Adc():
|
|
83
|
+
|
|
84
|
+
def __init__(self, duration, samples):
|
|
85
|
+
self.type = 'adc'
|
|
86
|
+
self.duration = duration
|
|
87
|
+
self.samples = samples
|
|
88
|
+
self.freq = 0
|
|
89
|
+
self.phase = 0
|
|
90
|
+
|
|
91
|
+
def set_freqphase(self, freq_phase):
|
|
92
|
+
self.freq = freq_phase[0]
|
|
93
|
+
self.phase = freq_phase[1]
|
|
94
|
+
|
|
95
|
+
class Trig():
|
|
96
|
+
|
|
97
|
+
def __init__(self, duration, trig_type):
|
|
98
|
+
self.type = 'trig'
|
|
99
|
+
self.duration = duration
|
|
100
|
+
self.trig_type = trig_type
|
|
101
|
+
|
|
102
|
+
class Sequence():
|
|
103
|
+
|
|
104
|
+
def __init__(self, ref_volt):
|
|
105
|
+
self.n_blocks = 0
|
|
106
|
+
self.duration = 0
|
|
107
|
+
self.block_list = []
|
|
108
|
+
|
|
109
|
+
self.rf = np.array([])
|
|
110
|
+
self.gx = np.array([])
|
|
111
|
+
self.gy = np.array([])
|
|
112
|
+
self.gz = np.array([])
|
|
113
|
+
|
|
114
|
+
# raster times
|
|
115
|
+
self.delta_rf = 5 # RF raster times in dsv file [us]
|
|
116
|
+
self.delta_grad = 10 # Gradient raster times in dsv file [us]
|
|
117
|
+
|
|
118
|
+
# Conversion factors from dsv to (Py)Pulseq (SI) units
|
|
119
|
+
self.gamma = 42.576e6
|
|
120
|
+
self.cf_time = 1e-6 # [us] -> [s]
|
|
121
|
+
self.cf_grad = 1e-3*self.gamma # [mT/m] -> [Hz/m]
|
|
122
|
+
|
|
123
|
+
# ref_volt is voltage for 1ms 180 deg rectangular pulse
|
|
124
|
+
# -> pulse [Hz] = pulse[V]/ref_volt[V] * 500[Hz]
|
|
125
|
+
self.cf_rf = 5e2 / ref_volt # [V] -> [Hz]
|
|
126
|
+
|
|
127
|
+
# trigger types
|
|
128
|
+
self.trig_types = {'EXTRIG0': 'ext1', 'OSC0': 'osc0', 'OSC1': 'osc1'}
|
|
129
|
+
|
|
130
|
+
# coil lead & hold times
|
|
131
|
+
self.rf_lead_time = 100 # [us]
|
|
132
|
+
self.rf_hold_time = 30 # [us]
|
|
133
|
+
|
|
134
|
+
# ADC dead time
|
|
135
|
+
self.adc_dead_time = 10 # [us]
|
|
136
|
+
|
|
137
|
+
def add_block(self, idx, duration, start_time):
|
|
138
|
+
self.n_blocks += 1
|
|
139
|
+
self.duration = start_time + duration
|
|
140
|
+
self.block_list.append(Block(idx, duration, start_time))
|
|
141
|
+
|
|
142
|
+
def get_block(self, idx):
|
|
143
|
+
return self.block_list[idx]
|
|
144
|
+
|
|
145
|
+
def set_shapes(self, shapes):
|
|
146
|
+
|
|
147
|
+
rfd = shapes[0].values * shapes[0].definitions.maxlimit / np.max(shapes[0].values)
|
|
148
|
+
rf = rfd * np.exp(1j*np.deg2rad(shapes[1].values))
|
|
149
|
+
self.rf = rf
|
|
150
|
+
self.gx = shapes[2].values
|
|
151
|
+
self.gy = shapes[3].values
|
|
152
|
+
self.gz = shapes[4].values
|
|
153
|
+
self.delta_rf = int(shapes[0].definitions.horidelta)
|
|
154
|
+
self.delta_grad= int(shapes[2].definitions.horidelta)
|
|
155
|
+
|
|
156
|
+
def get_shape(self, event):
|
|
157
|
+
"""
|
|
158
|
+
Gets shape for RF or gradient event
|
|
159
|
+
|
|
160
|
+
Logical coordinates 'gr', 'gp', 'gs' only map to the physical coordinates
|
|
161
|
+
when the sequence was simulated with transversal orientation and PE direction A->P
|
|
162
|
+
"""
|
|
163
|
+
if event.type == 'rf':
|
|
164
|
+
return self.rf[event.shape_ix]
|
|
165
|
+
elif event.type == 'gx' or event.type == 'gr':
|
|
166
|
+
return self.gx[event.shape_ix]
|
|
167
|
+
elif event.type == 'gy' or event.type == 'gp':
|
|
168
|
+
return self.gy[event.shape_ix]
|
|
169
|
+
elif event.type == 'gz' or event.type == 'gs':
|
|
170
|
+
return self.gz[event.shape_ix]
|
|
171
|
+
else:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def set_lead_hold_time(self, rf_lead_time, rf_hold_time):
|
|
175
|
+
"""
|
|
176
|
+
Set RF lead and hold times
|
|
177
|
+
"""
|
|
178
|
+
self.rf_lead_time = rf_lead_time
|
|
179
|
+
self.rf_hold_time = rf_hold_time
|
|
180
|
+
|
|
181
|
+
def set_adc_dead_time(self, adc_dead_time):
|
|
182
|
+
"""
|
|
183
|
+
Set ADC dead time
|
|
184
|
+
"""
|
|
185
|
+
self.adc_dead_time = adc_dead_time
|
|
186
|
+
|
|
187
|
+
def make_pulseq_sequence(self, filename=None, fov=[None, None, None], highgain=False):
|
|
188
|
+
"""
|
|
189
|
+
Create a Pulseq file from the sequence object.
|
|
190
|
+
Time tracking is always done in [us] (integers),
|
|
191
|
+
time is only converted to [s], when creating a pulseq event
|
|
192
|
+
|
|
193
|
+
Inputs:
|
|
194
|
+
--------
|
|
195
|
+
filename: If provided, Pulseq sequence will be saved to this file.
|
|
196
|
+
fov: Field of view in [mm] for x, y, z direction, which is set in the Pulseq file
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
logging.info("Create Pulseq sequence file.")
|
|
200
|
+
start_time = time.time()
|
|
201
|
+
|
|
202
|
+
filename = os.path.splitext(filename)[0] + '.seq'
|
|
203
|
+
|
|
204
|
+
system = pp.Opts(
|
|
205
|
+
max_grad=1e4,
|
|
206
|
+
max_slew=1e4,
|
|
207
|
+
rf_raster_time=self.delta_rf * self.cf_time,
|
|
208
|
+
grad_raster_time=self.delta_grad * self.cf_time,
|
|
209
|
+
grad_unit='mT/m',
|
|
210
|
+
slew_unit='mT/m/ms',
|
|
211
|
+
rf_ringdown_time=self.rf_hold_time * self.cf_time,
|
|
212
|
+
rf_dead_time=self.rf_lead_time * self.cf_time,
|
|
213
|
+
adc_dead_time=self.adc_dead_time * self.cf_time
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
pp_seq = pp.Sequence(system=system)
|
|
217
|
+
pp_seq.set_definition('Name', os.path.basename(filename))
|
|
218
|
+
if all(fov):
|
|
219
|
+
pp_seq.set_definition("FOV", [fov[0]*1e-3, fov[1]*1e-3, fov[2]*1e-3])
|
|
220
|
+
if highgain:
|
|
221
|
+
pp_seq.set_definition("ReceiverGainHigh", 1)
|
|
222
|
+
|
|
223
|
+
pulseq_events = self.__init_event_dict()
|
|
224
|
+
ts_offset = 0
|
|
225
|
+
|
|
226
|
+
if not self.block_list:
|
|
227
|
+
warn("No blocks in sequence. Please check the input DSV files. Exiting.")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Shift RF and ADC timestamps to account for lead and dead times
|
|
231
|
+
block_list = self.make_pulseq_block_list()
|
|
232
|
+
|
|
233
|
+
for ix, block in enumerate(block_list):
|
|
234
|
+
if ix == 1:
|
|
235
|
+
pass
|
|
236
|
+
block_offset = block_list[ix - 1].block_duration if ix > 0 else 0
|
|
237
|
+
ts_offset -= block_offset # offset time if Siemens block is splitted
|
|
238
|
+
for ts in block.timestamps:
|
|
239
|
+
events = block.timestamps[ts]
|
|
240
|
+
concat_g = {'gx': False, 'gy': False, 'gz': False}
|
|
241
|
+
pulseq_time = ts - ts_offset # timestamp in the current Pulseq block [us]
|
|
242
|
+
|
|
243
|
+
# check if any event exceeds the current timestamp
|
|
244
|
+
event_exceeds = {}
|
|
245
|
+
for key, event in pulseq_events.items():
|
|
246
|
+
if event is not None:
|
|
247
|
+
duration = round(pp.calc_duration(event) / self.cf_time)
|
|
248
|
+
event_exceeds[key] = duration > pulseq_time
|
|
249
|
+
else:
|
|
250
|
+
event_exceeds[key] = False
|
|
251
|
+
exceeded = any(event_exceeds.values())
|
|
252
|
+
|
|
253
|
+
if exceeded and any(pulseq_events[event.type] is not None for event in events):
|
|
254
|
+
# Check if the block has to be split (Pulseq only allows one event per channel per block)
|
|
255
|
+
# Gradients are either splitted or concatenated, depending on the presence of ADC, RF or Trigger events
|
|
256
|
+
split = not (event_exceeds['rf'] or event_exceeds['adc'] or event_exceeds['trig'])
|
|
257
|
+
if split: # split block
|
|
258
|
+
split_pt = round(pulseq_time / self.delta_grad)
|
|
259
|
+
pulseq_events_tmp = self.__init_event_dict()
|
|
260
|
+
for key, pp_event in pulseq_events.items():
|
|
261
|
+
if pp_event is None:
|
|
262
|
+
continue
|
|
263
|
+
pp_dur = round(pp.calc_duration(pp_event) / self.cf_time)
|
|
264
|
+
if pp_dur > pulseq_time:
|
|
265
|
+
if hasattr(pp_event, 'waveform') or hasattr(pp_event, 'amplitude'):
|
|
266
|
+
g_pre, g_post = self.__split_gradients(pp_event, split_pt, system)
|
|
267
|
+
pulseq_events_tmp[key] = g_post
|
|
268
|
+
pulseq_events[key] = g_pre
|
|
269
|
+
else:
|
|
270
|
+
raise ValueError(f"Event with type {pp_event.type} in block with index {block.block_idx} "
|
|
271
|
+
"cannot be split. Only gradients can be split.")
|
|
272
|
+
if any(pulseq_events.values()):
|
|
273
|
+
pp_seq.add_block(*self.__extract_events(pulseq_events))
|
|
274
|
+
pulseq_events = pulseq_events_tmp.copy()
|
|
275
|
+
ts_offset = round(ts / self.delta_grad) * self.delta_grad
|
|
276
|
+
else: # concatenate gradients
|
|
277
|
+
for event in events:
|
|
278
|
+
if pulseq_events[event.type] is not None:
|
|
279
|
+
pp_event = pulseq_events[event.type]
|
|
280
|
+
if event.type[0] == 'g':
|
|
281
|
+
g_conc = self.__make_pp_grad(event, pulseq_time, system)
|
|
282
|
+
if g_conc is not None:
|
|
283
|
+
g_wf = [waveform_from_seqblock(pp_event, system=system), waveform_from_seqblock(g_conc, system=system)]
|
|
284
|
+
len_wf = max([len(wf) for wf in g_wf])
|
|
285
|
+
g_wf = [np.concatenate([wf, np.zeros(len_wf - len(wf))]) for wf in g_wf]
|
|
286
|
+
g_wf = g_wf[0] + g_wf[1]
|
|
287
|
+
pulseq_events[event.type] = self.__make_arbitrary_grad(g_wf, event.channel, 0, system=system)
|
|
288
|
+
concat_g[event.type] = True
|
|
289
|
+
else:
|
|
290
|
+
raise ValueError(f"Event with type {pp_event.type} in block with index {block.block_idx} "
|
|
291
|
+
"cannot be concatenated. Only gradients can be concatenated.")
|
|
292
|
+
elif not exceeded:
|
|
293
|
+
# no split or concatenation needed, start new block
|
|
294
|
+
block_dur = pp.calc_duration(*self.__extract_events(pulseq_events))
|
|
295
|
+
block_dur_rounded = round_up_to_raster(pp.calc_duration(*self.__extract_events(pulseq_events)), 5)
|
|
296
|
+
if abs(block_dur - block_dur_rounded) > 1e-7:
|
|
297
|
+
pulseq_events['delay'] = pp.make_delay(block_dur_rounded)
|
|
298
|
+
if any(pulseq_events.values()):
|
|
299
|
+
pp_seq.add_block(*self.__extract_events(pulseq_events))
|
|
300
|
+
|
|
301
|
+
# Add remaining delay to the new block
|
|
302
|
+
delay = round((ts - ts_offset) * self.cf_time, 5)
|
|
303
|
+
delay_remain = delay - block_dur_rounded
|
|
304
|
+
pulseq_events = self.__init_event_dict()
|
|
305
|
+
if delay_remain > 0:
|
|
306
|
+
pulseq_events['delay'] = pp.make_delay(delay_remain)
|
|
307
|
+
ts_offset += round(block_dur_rounded / self.cf_time)
|
|
308
|
+
|
|
309
|
+
# add events to the block
|
|
310
|
+
for event in events:
|
|
311
|
+
event_del = ts - ts_offset
|
|
312
|
+
if event_del < 0:
|
|
313
|
+
raise ValueError("Negative event delay encountered.")
|
|
314
|
+
if event.type == 'rf':
|
|
315
|
+
event_del += self.rf_lead_time
|
|
316
|
+
rf = self.__make_pp_rf(event, event_del, system)
|
|
317
|
+
pulseq_events['rf'] = rf
|
|
318
|
+
elif event.type in ['gx', 'gy', 'gz']:
|
|
319
|
+
if not concat_g[event.type]:
|
|
320
|
+
g = self.__make_pp_grad(event, event_del, system)
|
|
321
|
+
if g is not None:
|
|
322
|
+
pulseq_events[event.type] = g
|
|
323
|
+
elif event.type == 'adc':
|
|
324
|
+
event_del += self.adc_dead_time
|
|
325
|
+
adc_dur = round(event.duration * self.cf_time, 7) # ADCs can be on nanosecond raster
|
|
326
|
+
adc_del = round(event_del * self.cf_time, 6)
|
|
327
|
+
adc = pp.make_adc(num_samples=event.samples, duration=adc_dur, delay=adc_del, freq_offset=event.freq, phase_offset=np.deg2rad(event.phase), system=system)
|
|
328
|
+
pulseq_events['adc'] = adc
|
|
329
|
+
elif event.type == 'trig':
|
|
330
|
+
trig_dur = round(event.duration * self.cf_time, 6)
|
|
331
|
+
trig_del = round(event_del * self.cf_time, 6)
|
|
332
|
+
if event.trig_type in self.trig_types:
|
|
333
|
+
trig = pp.make_digital_output_pulse(channel=self.trig_types[event.trig_type], duration=trig_dur, delay=trig_del, system=system)
|
|
334
|
+
pulseq_events['trig'] = trig
|
|
335
|
+
else:
|
|
336
|
+
delay = trig_dur + trig_del
|
|
337
|
+
if self.__check_delay(pulseq_events, delay):
|
|
338
|
+
pulseq_events['trig'] = pp.make_delay(d=delay)
|
|
339
|
+
logging.info(f'Unknown trigger type {event.trig_type} in block {block.block_idx} at time stamp {ts}. Replaced with delay.')
|
|
340
|
+
|
|
341
|
+
# add last block
|
|
342
|
+
delay = round((ts - ts_offset) * self.cf_time, 5)
|
|
343
|
+
if self.__check_delay(pulseq_events, delay):
|
|
344
|
+
pulseq_events['delay'] = pp.make_delay(delay) # account for possible delay in last block
|
|
345
|
+
if any(pulseq_events.values()):
|
|
346
|
+
pp_seq.add_block(*self.__extract_events(pulseq_events))
|
|
347
|
+
|
|
348
|
+
# check timing of the sequence
|
|
349
|
+
# set raster time to 1us for timing check, as otherwise ADC delays will throw errors
|
|
350
|
+
pp_seq.system.rf_raster_time = 1e-6
|
|
351
|
+
ok, error_report = pp_seq.check_timing()
|
|
352
|
+
pp_seq.system.rf_raster_time = self.delta_rf * self.cf_time
|
|
353
|
+
if not ok:
|
|
354
|
+
warn(f"PyPulseq timing check failed with {len(error_report)} errors.")
|
|
355
|
+
|
|
356
|
+
# write sequence
|
|
357
|
+
pp_seq.write(filename, check_timing=False)
|
|
358
|
+
end_time = time.time()
|
|
359
|
+
logging.info(f"Finished creating Pulseq file in {(end_time - start_time):.2f}s.")
|
|
360
|
+
|
|
361
|
+
return pp_seq
|
|
362
|
+
|
|
363
|
+
def __init_event_dict(self):
|
|
364
|
+
return {'rf': None, 'gx': None, 'gy': None, 'gz': None, 'adc': None, 'trig': None, 'delay': None}
|
|
365
|
+
|
|
366
|
+
def __extract_events(self, event_dict):
|
|
367
|
+
return [event for event in event_dict.values() if event is not None]
|
|
368
|
+
|
|
369
|
+
def __make_pp_rf(self, rf_event, event_del, system):
|
|
370
|
+
"""
|
|
371
|
+
Make a Pulseq RF event
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
rf_sig = self.get_shape(rf_event)
|
|
375
|
+
rf_del = round(event_del*self.cf_time, 6)
|
|
376
|
+
rf = pp.make_arbitrary_rf(signal=rf_sig, flip_angle=1, delay=rf_del, freq_offset=rf_event.freq, phase_offset=np.deg2rad(rf_event.phase), return_gz=False, system=system)
|
|
377
|
+
rf.signal = rf_sig * self.cf_rf # reset the signal as it gets scaled in make_arbitrary_rf
|
|
378
|
+
return rf
|
|
379
|
+
|
|
380
|
+
def __make_pp_grad(self, grad_event, event_del, system):
|
|
381
|
+
"""
|
|
382
|
+
Make a Pulseq gradient event
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
if grad_event.duration == 0 and grad_event.ramp_up == 0:
|
|
386
|
+
# zero duration gradient
|
|
387
|
+
return None
|
|
388
|
+
else:
|
|
389
|
+
g_wf = self.get_shape(grad_event) * self.cf_grad
|
|
390
|
+
g_del = round(event_del*self.cf_time, 5)
|
|
391
|
+
return self.__make_arbitrary_grad(g_wf, grad_event.channel, delay=g_del, system=system)
|
|
392
|
+
|
|
393
|
+
def __make_arbitrary_grad(self, wf, channel, delay, system):
|
|
394
|
+
|
|
395
|
+
if len(wf) > 1:
|
|
396
|
+
grad = pp.make_arbitrary_grad(channel=channel, waveform=wf, delay=delay, system=system)
|
|
397
|
+
else:
|
|
398
|
+
# Pypulseq throws an error if the waveform has only one point.
|
|
399
|
+
grad = pp.make_arbitrary_grad(
|
|
400
|
+
channel=channel,
|
|
401
|
+
waveform=np.concatenate([wf, np.zeros(1, dtype=float)]),
|
|
402
|
+
delay=0,
|
|
403
|
+
system=system
|
|
404
|
+
)
|
|
405
|
+
grad.waveform = wf
|
|
406
|
+
grad.tt = (np.arange(len(wf)) + 0.5) * system.grad_raster_time
|
|
407
|
+
grad.shape_dur = len(wf) * system.grad_raster_time
|
|
408
|
+
grad.first = wf[0]
|
|
409
|
+
grad.last = wf[0]
|
|
410
|
+
grad.area = np.sum(wf * system.grad_raster_time)
|
|
411
|
+
return grad
|
|
412
|
+
|
|
413
|
+
def __split_gradients(self, grad_event, split_pt, system):
|
|
414
|
+
"""
|
|
415
|
+
Split gradients at the split point.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
g_wf = waveform_from_seqblock(grad_event, system=system) # already contains delay
|
|
419
|
+
g_pre_wf = g_wf[:split_pt]
|
|
420
|
+
g_post_wf = g_wf[split_pt:]
|
|
421
|
+
|
|
422
|
+
g_pre = self.__make_arbitrary_grad(g_pre_wf, grad_event.channel, 0, system)
|
|
423
|
+
g_post = self.__make_arbitrary_grad(g_post_wf, grad_event.channel, 0, system)
|
|
424
|
+
return g_pre, g_post
|
|
425
|
+
|
|
426
|
+
def __check_delay(self, pulseq_events, delay):
|
|
427
|
+
"""
|
|
428
|
+
Check if delay is the longest delay in the current event block
|
|
429
|
+
"""
|
|
430
|
+
pulseq_events = self.__extract_events(pulseq_events)
|
|
431
|
+
delays = [item.delay for item in pulseq_events if (item.type == "delay" and item is not None)] + [0]
|
|
432
|
+
if delay > max(delays):
|
|
433
|
+
return True
|
|
434
|
+
else:
|
|
435
|
+
return False
|
|
436
|
+
|
|
437
|
+
def make_pulseq_block_list(self):
|
|
438
|
+
"""
|
|
439
|
+
Restructures the block list to simplify writing the Pulseq file. This includes:
|
|
440
|
+
- RF events are shifted by the RF lead time to not violate it
|
|
441
|
+
- ADC events are shifted by the ADC dead time to not violate it
|
|
442
|
+
|
|
443
|
+
Additionally, gradient events are created on all axes for all time periods where a gradient is present on any axis.
|
|
444
|
+
Gradient timing from the INF file is in logical coordinates, while gradient waveforms (GRX/GRY/GRZ) are in physical coordinates.
|
|
445
|
+
Since the rotation matrix is unavailable, we conservatively assume all axes are active wherever any axis has gradient activity.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
--------
|
|
449
|
+
A copy of self.block_list with 'rf' events shifted earlier by self.rf_lead_time.
|
|
450
|
+
The original self.block_list remains unchanged.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
# Deep copy to avoid modifying original blocks or event lists
|
|
454
|
+
block_list_shifted = copy.deepcopy(self.block_list)
|
|
455
|
+
|
|
456
|
+
grad_offset = {'x': 0, 'y': 0, 'z': 0}
|
|
457
|
+
for block in block_list_shifted:
|
|
458
|
+
shifted_timestamps = {}
|
|
459
|
+
grad_ts = {'x': 0, 'y': 0, 'z': 0}
|
|
460
|
+
grad_end_last = {'x': 0, 'y': 0, 'z': 0}
|
|
461
|
+
grads = {'x': None, 'y': None, 'z': None}
|
|
462
|
+
|
|
463
|
+
for ts_str in block.timestamps:
|
|
464
|
+
ts = int(ts_str)
|
|
465
|
+
events = block.timestamps[ts_str]
|
|
466
|
+
|
|
467
|
+
if not events:
|
|
468
|
+
shifted_timestamps.setdefault(ts, [])
|
|
469
|
+
|
|
470
|
+
for event in events:
|
|
471
|
+
if event.type == 'rf':
|
|
472
|
+
new_ts = ts - self.rf_lead_time
|
|
473
|
+
shifted_timestamps.setdefault(new_ts, []).append(event)
|
|
474
|
+
|
|
475
|
+
elif event.type == 'adc':
|
|
476
|
+
new_ts = ts - self.adc_dead_time
|
|
477
|
+
shifted_timestamps.setdefault(new_ts, []).append(event)
|
|
478
|
+
|
|
479
|
+
elif event.type[0] == 'g':
|
|
480
|
+
for axis, g in grads.items():
|
|
481
|
+
grad_start = max(ts, grad_offset[axis])
|
|
482
|
+
grad_dur = event.duration + event.ramp_dn
|
|
483
|
+
grad_end = ts + grad_dur
|
|
484
|
+
grad_dur = grad_end - grad_start # correct if offset > start
|
|
485
|
+
shape_ix = slice((block.start_time + grad_start) // self.delta_grad,
|
|
486
|
+
(block.start_time + grad_start + grad_dur) // self.delta_grad)
|
|
487
|
+
|
|
488
|
+
if grad_start >= grad_end_last[axis] and grad_end > grad_offset[axis]:
|
|
489
|
+
if g is not None:
|
|
490
|
+
shifted_timestamps.setdefault(grad_ts[axis], []).append(g)
|
|
491
|
+
|
|
492
|
+
g = Grad(axis, 0, grad_dur, grad_dur, 0, shape_ix)
|
|
493
|
+
if any(self.get_shape(g)):
|
|
494
|
+
grads[axis] = g
|
|
495
|
+
grad_ts[axis] = grad_start
|
|
496
|
+
grad_end_last[axis] = grad_end
|
|
497
|
+
|
|
498
|
+
elif grad_end > grad_end_last[axis]:
|
|
499
|
+
grad_dur_new = g.duration + grad_end - grad_end_last[axis]
|
|
500
|
+
shape_end_new = g.shape_ix.stop + (grad_end - grad_end_last[axis]) // self.delta_grad
|
|
501
|
+
g.duration = g.ramp_up = grad_dur_new
|
|
502
|
+
g.shape_ix = slice(g.shape_ix.start, shape_end_new)
|
|
503
|
+
grad_end_last[axis] = grad_end
|
|
504
|
+
|
|
505
|
+
else:
|
|
506
|
+
shifted_timestamps.setdefault(ts, []).append(event)
|
|
507
|
+
|
|
508
|
+
for axis, g in grads.items():
|
|
509
|
+
grad_offset[axis] = grad_end_last[axis] - block.block_duration # gradients can extend beyond the block duration
|
|
510
|
+
if g is not None:
|
|
511
|
+
shifted_timestamps.setdefault(grad_ts[axis], []).append(g)
|
|
512
|
+
|
|
513
|
+
block.timestamps = dict(
|
|
514
|
+
sorted(((ts, evts) for ts, evts in shifted_timestamps.items()), key=lambda x: int(x[0]))
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return block_list_shifted
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dsv2pulseq
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Create Pulseq sequences from dsv.
|
|
5
|
+
Author-email: Marten Veldmann <marten.veldmann@dzne.de>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://github.com/mavel101/dsv2pulseq
|
|
8
|
+
Keywords: pulseq,dsv,siemens,mri
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: matplotlib
|
|
14
|
+
Requires-Dist: pypulseq==1.4.2.post1
|
|
15
|
+
Requires-Dist: ismrmrd==1.14.1
|
|
16
|
+
Requires-Dist: twixtools==0.23
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# dsv2pulseq
|
|
20
|
+
|
|
21
|
+
Create Pulseq sequence files from Siemens dsv simulation files. The converter was only tested with VE/VX dsv file layouts.
|
|
22
|
+
VB/VD files are not supported, as the simulator can not output RF phase data.
|
|
23
|
+
RF values might be differing slightly compared to the original sequence, as the dsv files contain RF values only on a 5us raster.
|
|
24
|
+
|
|
25
|
+
## Dependencies & Installation
|
|
26
|
+
|
|
27
|
+
A Python environment with the package installed can be created with the provided yml file: `conda env create -f dsv2pulseq.yml`.
|
|
28
|
+
Unittests can be run with `python -m unittest discover test/`. This includes testing, whether the conversion is successful for each dsv dataset in the folder "test/test_data".
|
|
29
|
+
|
|
30
|
+
This package only depends on numpy and PyPulseq [1].
|
|
31
|
+
|
|
32
|
+
Merging data to the original Siemens raw data file for retrospective reconstruction (see "Reconstruction of Pulseq data") requires the twixtools package (https://github.com/pehses/twixtools).
|
|
33
|
+
|
|
34
|
+
## Sequence simulation
|
|
35
|
+
|
|
36
|
+
Simulate the sequence with the following settings:
|
|
37
|
+
- Transversal orientation with phase-encode direction A->P and no FOV shift (which is the default)
|
|
38
|
+
- Simulate with RF phase output (sim /RFP+).
|
|
39
|
+
- Mandatory dsv files are "_INF", "_RFD", "_RFP", "_GRX", "_GRY" and "_GRZ".
|
|
40
|
+
|
|
41
|
+
Example data "MiniFLASH.dsv" can be found in the test/test_data folder. These simulation files are from the Siemens MiniFLASH demo sequence.
|
|
42
|
+
|
|
43
|
+
## Create Pulseq output
|
|
44
|
+
|
|
45
|
+
The conversion can be started by running:
|
|
46
|
+
```
|
|
47
|
+
dsv_to_pulseq -o OUT_FILE -r REF_VOLT IN_FILE_PREFIX
|
|
48
|
+
```
|
|
49
|
+
from the shell.
|
|
50
|
+
|
|
51
|
+
The IN_FILE_PREFIX is the prefix of the dsv files, e.g. "gre" for "gre_XXX.dsv". The OUT_FILE is the Pulseq output sequence file (default: "external.seq"). The reference voltage is the voltage the sequence was simulated with (Siemens default: 223.529007 V).
|
|
52
|
+
|
|
53
|
+
The conversion can also be done in Python by running:
|
|
54
|
+
```
|
|
55
|
+
from dsv2pulseq import read_dsv
|
|
56
|
+
seq = read_dsv('/path/to/dsv/dsv_prefix')
|
|
57
|
+
seq_pulseq = seq.make_pulseq_sequence('external.seq')
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
There is an experimental function to check the shapes of RF waveforms and gradients that plots the difference between the original and converted waveforms:
|
|
61
|
+
```
|
|
62
|
+
from dsv2pulseq import check_dsv
|
|
63
|
+
check_dsv('/path_to_dsv/dsv_prefix_original', 'path_to_dsv/dsv_prefix_pulseq')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Note that:
|
|
67
|
+
- Only single transmit channel RF pulses are currently supported.
|
|
68
|
+
- The Pulseq sequence has the same orientation as the original sequence in the physical/scanner coordinate system, when running the Pulseq interpreter in "XYZ in TRA" mode.
|
|
69
|
+
- The Pulseq interpreter version 1.5.0 should be used, as the versions before contain a bug in setting the RF raster time correctly.
|
|
70
|
+
- RF and gradient waveforms might be slightly different compared to the original sequence due to limited numerical accuracy of the DSV files.
|
|
71
|
+
- RF pulse values are on a 5us raster in the dsv files, which might be insufficient for pulses with rapidly varying phase
|
|
72
|
+
|
|
73
|
+
## Reconstruction of Pulseq data
|
|
74
|
+
|
|
75
|
+
Data acquired with the converted Pulseq sequence cannot be easily reconstructed, as the scan headers containing the meta information are missing.
|
|
76
|
+
The data from the Pulseq sequence (IN_FILE_2) can be inserted into a raw data file acquired with the original sequence (IN_FILE_1) using twixtools (see dependencies). If twixtools is installed, the following command will insert the data:
|
|
77
|
+
```
|
|
78
|
+
insert_twix_data IN_FILE_1 IN_FILE_2 -o OUT_FILE
|
|
79
|
+
```
|
|
80
|
+
The merged output file (OUT_FILE) can be used for rectrospective reconstruction at the scanner. Alternatively the Pulseq data file can also be in the MRD format (requires ISMRMRD python package)
|
|
81
|
+
|
|
82
|
+
## References
|
|
83
|
+
|
|
84
|
+
[1] Ravi, Keerthi, Sairam Geethanath, and John Vaughan. "PyPulseq: A Python Package for MRI Pulse Sequence Design." Journal of Open Source Software 4.42 (2019): 1725., https://github.com/imr-framework/pypulseq
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dsv2pulseq/__init__.py,sha256=owW-fIyzHOvugm4hJwBXLvSLfjjCKJ76obKteZsXZCU,41
|
|
2
|
+
dsv2pulseq/dsv_to_pulseq.py,sha256=N_CKKwWOBKst8aQ9cFdMIHu4gLIXv4TQJko17BGmR9Y,1867
|
|
3
|
+
dsv2pulseq/helper.py,sha256=irbn8beTvuSnSVBXP0r3Bs1trOg4BhPYQ8L4KXkLoaw,1633
|
|
4
|
+
dsv2pulseq/insert_twix_data.py,sha256=ap6NwrdO9g1_ZyyJ-eIUyesFIEkd_TkTsWCQYGUnIMo,2394
|
|
5
|
+
dsv2pulseq/read_dsv.py,sha256=6YgAk7Ex887uyLMCHaIcDcx3n6aeOQcQqg4FATLcWsc,6216
|
|
6
|
+
dsv2pulseq/read_dsv_inf.py,sha256=9InZBXvRsBwZLKO6gmCZ9FAAaXLlGESdYuLckMpt2p8,4959
|
|
7
|
+
dsv2pulseq/read_dsv_samples.py,sha256=OSVwsFNv37JBAAwJhg3Z2SXVRCB7xaBUo3vQlVqTTF8,4950
|
|
8
|
+
dsv2pulseq/sequence.py,sha256=ogVnsfMgeC07rghViObZgY3Tz979VOnxC-o97Tn3SgI,22793
|
|
9
|
+
dsv2pulseq-0.0.0.dist-info/licenses/LICENSE,sha256=BLTe_Dlta_WTwEeHsZVasjaklHAx6IFpWguMWroxs4U,1072
|
|
10
|
+
dsv2pulseq-0.0.0.dist-info/METADATA,sha256=qZf-DziA4OSRfHxTo3SUdMlGm48o-vKB2MKz98H7lQw,4320
|
|
11
|
+
dsv2pulseq-0.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
dsv2pulseq-0.0.0.dist-info/entry_points.txt,sha256=9r2VipfrgHXLDTwsGeyCK7XyJ0lcdycCDMJO4lVc4Bo,116
|
|
13
|
+
dsv2pulseq-0.0.0.dist-info/top_level.txt,sha256=HE9Nz1e1uYO0SPUX-Ub868nHguGulocRXGUgAEi3S4g,11
|
|
14
|
+
dsv2pulseq-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Marten Veldmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dsv2pulseq
|