seq2xml_fixed_delay.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. # seq2xml.py : converts Pulseq (.seq) files into JEMRIS (.xml) sequences
  2. # Gehua Tong
  3. # March 2020
  4. # from pypulseq.Sequence.sequence import Sequence
  5. # from pypulseq.calc_duration import calc_duration
  6. import pypulseq as pp
  7. import xml.etree.ElementTree as ET
  8. import h5py
  9. import numpy as np
  10. from math import pi
  11. def save_rf_library_info(seq, out_folder):
  12. """
  13. Helper function that stores distinct RF waveforms for seq2xml
  14. """
  15. # RF library
  16. rf_shapes_path_dict = {}
  17. for rf_id in list(seq.rf_library.data.keys()): # for each RF ID
  18. # JEMRIS wants:
  19. # "Filename": A HDF5-file with a single dataset "extpulse"
  20. # of size N x 3 where the 1st column holds the time points,
  21. # and 2nd and 3rd column hold amplitudes and phases, respectively.
  22. # Phase units should be radians.
  23. # Time is assumed to increase and start at zero.
  24. # The last time point defines the length of the pulse.
  25. file_path_partial = f'rf_{int(rf_id)}.h5'
  26. file_path_full = out_folder + '/' + file_path_partial
  27. # De-compress using inbuilt PyPulseq method
  28. rf = seq.rf_from_lib_data(seq.rf_library.data[rf_id])
  29. # Only extract time, magnitude, and phase
  30. # We leave initial phase and freq offset to the main conversion loop)
  31. times = rf.t
  32. magnitude = np.absolute(rf.signal)
  33. phase = np.angle(rf.signal)
  34. N = len(magnitude)
  35. # Create file
  36. f = h5py.File(file_path_full, 'a')
  37. if "extpulse" in f.keys():
  38. del f["extpulse"]
  39. #f.create_dataset("extpulse", (N,3), dtype='f')
  40. f.create_dataset("extpulse",(3,N),dtype='f')
  41. times = times - times[0]
  42. f["extpulse"][0,:] = times*sec2ms#*sec2ms
  43. f["extpulse"][1,:] = magnitude*rf_const
  44. f["extpulse"][2,:] = phase#"Phase should be radians"
  45. f.close()
  46. rf_shapes_path_dict[rf_id] = file_path_partial
  47. return rf_shapes_path_dict
  48. # Helper function
  49. def save_grad_library_info(seq, out_folder):
  50. """
  51. Helper function that stores distinct gradients for seq2xml
  52. """
  53. #file_paths = [out_folder + f'grad_{int(grad_id)}.h5' for grad_id in range(1,N_grad_id+1)]
  54. grad_shapes_path_dict = {}
  55. processed_g_inds = []
  56. for nb in range(1,len(seq.block_events)+1):
  57. gx_ind, gy_ind, gz_ind = seq.block_events[nb][2:5]
  58. for axis_ind, g_ind in enumerate([gx_ind, gy_ind, gz_ind]):
  59. # Only save a gradient file if ...(a) it has non-zero index
  60. # (b) it is type 'grad', not 'trap'
  61. # s and (c) its index has not been processed
  62. if g_ind != 0 and len(seq.grad_library.data[g_ind]) == 3 \
  63. and g_ind not in processed_g_inds:
  64. print(f'Adding Gradient Number {g_ind}')
  65. this_block = seq.get_block(nb)
  66. file_path_partial = f'grad_{int(g_ind)}.h5'
  67. file_path_full = out_folder + '/' + file_path_partial
  68. #TODO make it work for x/y/z
  69. if axis_ind == 0:
  70. t_points = this_block.gx.t
  71. g_shape = this_block.gx.waveform
  72. elif axis_ind == 1:
  73. t_points = this_block.gy.t
  74. g_shape = this_block.gy.waveform
  75. elif axis_ind == 2:
  76. t_points = this_block.gz.t
  77. g_shape = this_block.gz.waveform
  78. N = len(t_points)
  79. # Create file
  80. f = h5py.File(file_path_full, 'a')
  81. if "extpulse" in f.keys():
  82. del f["extpulse"]
  83. f.create_dataset("extpulse", (2,N), dtype='f')
  84. f["extpulse"][0,:] = t_points * sec2ms
  85. f["extpulse"][1,:] = g_shape * g_const
  86. f.close()
  87. grad_shapes_path_dict[g_ind] = file_path_partial
  88. processed_g_inds.append(g_ind)
  89. return grad_shapes_path_dict
  90. # Notes
  91. # This is for generating an .xml file for input into JEMRIS simulator, from a Pulseq .seq file
  92. # The opposite philosophies make the .xml encoding suboptimal for storage
  93. # (because seq files consists of flattened-out Blocks while the JEMRIS format minimizes repetition using loops
  94. # and consists of many cross-referencing of parameters)
  95. # Consider: for virtual scanner, have scripts that generate .xml and .seq at the same time (looped vs. flattened)
  96. # (but isn't JEMRIS already doing that? JEMRIS does have an "output to pulseq" functionality)
  97. # though then, having a Python interface instead of a MATLAB one is helpful in the open-source aspect
  98. # Unit conversion constants (comment with units before & after)
  99. rf_const = 2 * pi / 1000 # from Pulseq[Hz]=[1/s] to JEMRIS[rad/ms] rf magnitude conversion constant
  100. g_const = 2 * pi / 1e6 # from Pulseq [Hz/m] to JEMRIS [(rad/ms)/mm] gradient conversion constant
  101. slew_const = g_const / 1000 # from Pulseq [Hz/(m*s)] to JEMRIS [(rad/ms)/(mm*ms)]
  102. ga_const = 2 * pi / 1000 # from Pulseq[1/m] to JEMRIS [2*pi/mm] gradient area conversion constant
  103. sec2ms = 1000 # time conversion constant
  104. rad2deg = 180/pi
  105. freq_const = 2 * pi / 1000 # From Hz to rad/ms
  106. #def seq2xml(seq, seq_name, out_folder):
  107. """
  108. # Takes a Pulseq sequence and converts it into .xml format for JEMRIS
  109. # All RF and gradient shapes are stored as .h5 files
  110. Inputs
  111. ------
  112. seq : pypulseq.Sequence.sequence.Sequence
  113. seq_name : name of output .xml file
  114. out_folder : str
  115. Path to output folder for .xml file
  116. Returns
  117. -------
  118. seq_tree : xml.etree.ElementTree
  119. Tree object used for generating the sequence .xml file
  120. seq_path : str
  121. Path to stored .xml sequence
  122. """
  123. seq = pp.Sequence()
  124. seq.read('C:\\MRI_seq\\pypulseq\\seq_examples\\new_scripts\\epi_se_pypulseq.seq')
  125. seq_name='epi_se_pypulseq.seq_fixed_delay'
  126. out_folder='C:\\MRI_seq\\pypulseq\\seq_examples\\new_scripts'
  127. #seq.read('C:\\MRI_seq\\new_MRI_pulse_seq\\t1_SE\\t1_SE_matrx32x32.seq')
  128. #seq_name='t1_SE_matrx32x32_fixed_delay'
  129. #out_folder='C:\\MRI_seq\\new_MRI_pulse_seq\\t1_SE'
  130. # Parameters is the root of the xml
  131. root = ET.Element("Parameters")
  132. # Add gradient limits (seem to be the only parameters shared by both formats)
  133. # TODO check units
  134. root.set("GradMaxAmpl", str(seq.system.max_grad*g_const))
  135. root.set("GradSlewRate", str(seq.system.max_slew*slew_const))
  136. # ConcatSequence is the element for the sequence itself;
  137. # Allows addition of multiple AtomicSequence
  138. C0 = ET.SubElement(root, "ConcatSequence")
  139. # Use helper functions to save all RF and only arbitrary gradient info
  140. rf_shapes_path_dict = save_rf_library_info(seq, out_folder)
  141. grad_shapes_path_dict = save_grad_library_info(seq, out_folder)
  142. #print(grad_shapes_path_dict)
  143. #///////////////////////////////////////////////////////////////////////////
  144. rf_name_ind = 1
  145. grad_name_ind = 1
  146. delay_name_ind = 1
  147. adc_name_ind = 1
  148. ##### Main loop! #####
  149. # Go through all blocks and add in events; each block is one AtomicSequence
  150. for block_ind in range(1,len(seq.block_events)+1):
  151. #for block_ind in range(3,4):
  152. blk = seq.get_block(block_ind).__dict__
  153. exists_adc = 'adc' in blk.keys()
  154. adc_already_added = False
  155. # Note: "EmptyPulse" class seems to allow variably spaced ADC sampling
  156. # Distinguish between delay and others
  157. # Question: in pulseq, does delay happen together with other events?
  158. # (for now, we assume delay always happens by itself)
  159. # About name of atomic sequences: not adding names for now;
  160. # (Likely it will cause no problems because there is no cross-referencing)
  161. C_block = ET.SubElement(C0, "ATOMICSEQUENCE")
  162. C_block.set("Name", f'C{block_ind}')
  163. for key in blk.keys():
  164. # Case of RF pulse
  165. if key == 'rf':
  166. rf = blk['rf']
  167. if type(rf) != type(None):
  168. rf_atom = ET.SubElement(C_block, "EXTERNALRFPULSE")
  169. rf_atom.set("Name", f'R{rf_name_ind}')
  170. rf_name_ind += 1
  171. rf_atom.set("InitialDelay", str(rf.delay*sec2ms))
  172. rf_atom.set("InitialPhase", str(rf.phase_offset*rad2deg))
  173. rf_atom.set("Frequency", str(rf.freq_offset*freq_const))
  174. # Find ID of this rf event
  175. rf_id = seq.block_events[block_ind][1]
  176. rf_atom.set("Filename", rf_shapes_path_dict[rf_id])
  177. rf_atom.set("Scale","1")
  178. rf_atom.set("Interpolate", "0") # Do interpolate
  179. gnames_map = {'gx':2, 'gy':3, 'gz':4}
  180. if key in ['gx', 'gy', 'gz']:
  181. g = blk[key]
  182. if type(g) != type(None):
  183. if g.type == "trap":
  184. if g.amplitude != 0:
  185. g_atom = ET.SubElement(C_block, "TRAPGRADPULSE")
  186. g_atom.set("Name", f'G{grad_name_ind}')
  187. grad_name_ind += 1
  188. if g.flat_time > 0:
  189. # 1. Case where flat_time is nonzero
  190. # Second, fix FlatTopArea and FlatTopTime
  191. g_atom.set("FlatTopArea", str(g.flat_area*ga_const))
  192. g_atom.set("FlatTopTime", str(g.flat_time*sec2ms))
  193. # Last, set axis and delay
  194. else:
  195. # 2. Case of triangular pulse (i.e. no flat part)
  196. g_atom.set("MaxAmpl", str(np.absolute(g.amplitude*g_const))) # limit amplitude
  197. g_atom.set("Area", str(0.5*(g.rise_time + g.fall_time)*g.amplitude*ga_const))
  198. # Third, limit duration by limiting slew rate
  199. g_atom.set("SlewRate", str((g.amplitude * g_const) / (g.fall_time * sec2ms)))
  200. g_atom.set("Asymmetric", str(g.fall_time / g.rise_time))
  201. g_atom.set("Axis", key.upper())
  202. g_atom.set("InitialDelay", str(g.delay*sec2ms))
  203. # Add ADC if it exists and then "mark as complete"
  204. elif g.type == "grad":
  205. # Set arbitrary grad parameters
  206. # Need to load h5 file again, just like in RF
  207. g_id = seq.block_events[block_ind][gnames_map[key]]
  208. g_atom = ET.SubElement(C_block, "EXTERNALGRADPULSE")
  209. g_atom.set("Name", f'G{grad_name_ind}')
  210. grad_name_ind += 1
  211. g_atom.set("Axis", key.upper())
  212. g_atom.set("Filename", grad_shapes_path_dict[g_id])
  213. g_atom.set("Scale","1")
  214. g_atom.set("Interpolate","0")
  215. g_atom.set("InitialDelay",str(g.delay*sec2ms))
  216. else:
  217. print(f'Gradient type "{g.type}" indicated')
  218. raise ValueError("Gradient's type should be either trap or grad")
  219. if exists_adc and not adc_already_added:
  220. adc = blk['adc']
  221. if type(adc) != type(None):
  222. dwell = adc.dwell*sec2ms
  223. adc_delay = adc.delay*sec2ms
  224. Nro = adc.num_samples
  225. gzero_adc = ET.SubElement(C_block, "TRAPGRADPULSE")
  226. gzero_adc.set("Name", f'S{adc_name_ind}')
  227. adc_name_ind += 1
  228. gzero_adc.set("ADCs", str(Nro))
  229. gzero_adc.set("FlatTopTime", str(dwell*Nro))
  230. gzero_adc.set("FlatTopArea","0")
  231. gzero_adc.set("InitialDelay", str(adc_delay))
  232. adc_already_added = True
  233. # Now, it always attach ADC to the first gradient found among keys()
  234. # This might be tricky
  235. # suggestion 1: check the duration of gradient?
  236. # suggestion 2: just do any gradient and hope it works
  237. # suggestion 3: read the JEMRIS documentation/try on GUI
  238. if type(blk['gx']) == type(None) and type(blk['gy']) == type(None) and type(blk['gz']) == type(None) and type(blk['rf']) == type(None) and type(blk['adc']) == type(None) :
  239. delay_dur = blk['block_duration']
  240. delay_atom = ET.SubElement(C0, "DELAYATOMICSEQUENCE")
  241. delay_atom.set("Name",f'D{delay_name_ind}')
  242. delay_name_ind += 1
  243. delay_atom.set("Delay",str(delay_dur*sec2ms))
  244. delay_atom.set("DelayType","B2E")
  245. # Output it!
  246. seq_tree = ET.ElementTree(root)
  247. seq_path = out_folder + '/' + seq_name + '.xml'
  248. seq_tree.write(seq_path)
  249. #return seq_tree, seq_path
  250. # if __name__ == '__main__':
  251. # print('')
  252. # seq = pp.Sequence()
  253. # seq.read('gre_pypulseq_1slice_for_sim.seq')
  254. # seq2xml(seq, seq_name='gre_test1', out_folder='test')
  255. # seq.read('seq_files/spgr_gspoil_N16_Ns1_TE5ms_TR10ms_FA30deg.seq')
  256. #seq.read('benchmark_seq2xml/gre_jemris.seq')
  257. # seq.read('try_seq2xml/spgr_gspoil_N15_Ns1_TE5ms_TR10ms_FA30deg.seq')
  258. #seq.read('orc_test/seq_2020-02-26_ORC_54_9_384_1.seq')
  259. #stree = seq2xml(seq, seq_name="ORC-Marina", out_folder='orc_test')