read_seq.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. import re
  2. from pathlib import Path
  3. from types import SimpleNamespace
  4. from typing import Dict, Tuple, List
  5. import numpy as np
  6. from seqgen.pypulseq.calc_duration import calc_duration
  7. from seqgen.pypulseq.compress_shape import compress_shape
  8. from seqgen.pypulseq.decompress_shape import decompress_shape
  9. from seqgen.pypulseq.event_lib import EventLibrary
  10. from seqgen.pypulseq.supported_labels_rf_use import get_supported_labels
  11. def read(self, path: str, detect_rf_use: bool = False) -> None:
  12. """
  13. Load sequence from file - read the given filename and load sequence data into sequence object.
  14. See also `pypulseq.Sequence.write_seq.write()`.
  15. Parameters
  16. ----------
  17. path : Path
  18. Path of sequence file to be read.
  19. detect_rf_use : bool, default=False
  20. Boolean flag to let the function infer the currently missing flags concerning the intended use of the RF pulses
  21. (excitation, refocusing, etc). These are important for the k-space trajectory calculation.
  22. Raises
  23. ------
  24. FileNotFoundError
  25. If no sequence file is found at `path`.
  26. RuntimeError
  27. If incompatible sequence files are attempted to be loaded.
  28. ValueError
  29. If unexpected sections are encountered when loading a sequence file.
  30. """
  31. try:
  32. input_file = open(path, "r")
  33. except FileNotFoundError as e:
  34. raise FileNotFoundError(e)
  35. # Event libraries
  36. self.adc_library = EventLibrary()
  37. self.grad_library = EventLibrary()
  38. self.label_inc_library = EventLibrary()
  39. self.label_set_library = EventLibrary()
  40. self.rf_library = EventLibrary()
  41. self.shape_library = EventLibrary()
  42. self.trigger_library = EventLibrary()
  43. # Raster times
  44. self.grad_raster_time = self.system.grad_raster_time
  45. self.rf_raster_time = self.system.rf_raster_time
  46. self.block_events = {}
  47. self.definitions = {}
  48. self.extension_string_IDs = {}
  49. self.extension_numeric_IDs = []
  50. jemris_generated = False
  51. version_combined = 0
  52. # Load data from file
  53. while True:
  54. section = __skip_comments(input_file)
  55. if section == -1:
  56. break
  57. if section == "[DEFINITIONS]":
  58. self.definitions = __read_definitions(input_file)
  59. # Gradient raster time
  60. if "GradientRasterTime" in self.definitions:
  61. self.gradient_raster_time = self.definitions["GradientRasterTime"]
  62. # Radio frequency raster time
  63. if "RadiofrequencyRasterTime" in self.definitions:
  64. self.rf_raster_time = self.definitions["RadiofrequencyRasterTime"]
  65. # ADC raster time
  66. if "AdcRasterTime" in self.definitions:
  67. self.adc_raster_time = self.definitions["AdcRasterTime"]
  68. # Block duration raster
  69. if "BlockDurationRaster" in self.block_events:
  70. self.block_duration_raster = self.block_events["BlockDurationRaster"]
  71. elif section == "[JEMRIS]":
  72. jemris_generated = True
  73. elif section == "[SIGNATURE]":
  74. temp_sign_defs = __read_definitions(input_file)
  75. if "Type" in temp_sign_defs:
  76. self.signature_type = temp_sign_defs["Type"]
  77. if "Hash" in temp_sign_defs:
  78. self.signature_value = temp_sign_defs["Hash"]
  79. self.signature_file = "Text"
  80. elif section == "[VERSION]":
  81. version_major, version_minor, version_revision = __read_version(input_file)
  82. if version_major != self.version_major:
  83. raise RuntimeError(
  84. f"Unsupported version_major: {version_major}. Expected: {self.version_major}"
  85. )
  86. version_combined = (
  87. 1000000 * version_major + 1000 * version_minor + version_revision
  88. )
  89. if version_combined < 1002000:
  90. raise RuntimeError(
  91. f"Unsupported version {version_major}.{version_minor}.{version_revision}, only file "
  92. f"format revision 1.2.0 and above are supported."
  93. )
  94. if version_combined < 1003001:
  95. raise RuntimeError(
  96. f"Loading older Pulseq format file (version "
  97. f"{version_major}.{version_minor}.{version_revision}) some code may function not as "
  98. f"expected"
  99. )
  100. elif section == "[BLOCKS]":
  101. if version_major == 0:
  102. raise RuntimeError(
  103. "Pulseq file MUST include [VERSION] section prior to [BLOCKS] section"
  104. )
  105. result = __read_blocks(
  106. input_file,
  107. block_duration_raster=self.block_duration_raster,
  108. version_combined=version_combined,
  109. )
  110. self.block_events, self.block_durations, delay_ind_temp = result
  111. elif section == "[RF]":
  112. if jemris_generated:
  113. self.rf_library = __read_events(
  114. input_file, (1, 1, 1, 1, 1), event_library=self.rf_library
  115. )
  116. else:
  117. if version_combined >= 1004000: # 1.4.x format
  118. self.rf_library = __read_events(
  119. input_file,
  120. (1, 1, 1, 1, 1e-6, 1, 1),
  121. event_library=self.rf_library,
  122. )
  123. else: # 1.3.x and below
  124. self.rf_library = __read_events(
  125. input_file, (1, 1, 1, 1e-6, 1, 1), event_library=self.rf_library
  126. )
  127. elif section == "[GRADIENTS]":
  128. if version_combined >= 1004000: # 1.4.x format
  129. self.grad_library = __read_events(
  130. input_file, (1, 1, 1, 1e-6), "g", self.grad_library
  131. )
  132. else: # 1.3.x and below
  133. self.grad_library = __read_events(
  134. input_file, (1, 1, 1e-6), "g", self.grad_library
  135. )
  136. elif section == "[TRAP]":
  137. if jemris_generated:
  138. self.grad_library = __read_events(
  139. input_file, (1, 1e-6, 1e-6, 1e-6), "t", self.grad_library
  140. )
  141. else:
  142. self.grad_library = __read_events(
  143. input_file, (1, 1e-6, 1e-6, 1e-6, 1e-6), "t", self.grad_library
  144. )
  145. elif section == "[ADC]":
  146. self.adc_library = __read_events(
  147. input_file, (1, 1e-9, 1e-6, 1, 1), event_library=self.adc_library
  148. )
  149. elif section == "[DELAYS]":
  150. if version_combined >= 1004000:
  151. raise RuntimeError(
  152. "Pulseq file revision 1.4.0 and above MUST NOT contain [DELAYS] section"
  153. )
  154. temp_delay_library = __read_events(input_file, (1e-6,))
  155. elif section == "[SHAPES]":
  156. self.shape_library = __read_shapes(
  157. input_file, version_major == 1 and version_minor < 4
  158. )
  159. elif section == "[EXTENSIONS]":
  160. self.extensions_library = __read_events(input_file)
  161. else:
  162. if section[:18] == "extension TRIGGERS":
  163. extension_id = int(section[18:])
  164. self.set_extension_string_ID("TRIGGERS", extension_id)
  165. self.trigger_library = __read_events(
  166. input_file, (1, 1, 1e-6, 1e-6), event_library=self.trigger_library
  167. )
  168. elif section[:18] == "extension LABELSET":
  169. extension_id = int(section[18:])
  170. self.set_extension_string_ID("LABELSET", extension_id)
  171. l1 = lambda s: int(s)
  172. l2 = lambda s: get_supported_labels().index(s) + 1
  173. self.label_set_library = __read_and_parse_events(input_file, l1, l2)
  174. elif section[:18] == "extension LABELINC":
  175. extension_id = int(section[18:])
  176. self.set_extension_string_ID("LABELINC", extension_id)
  177. l1 = lambda s: int(s)
  178. l2 = lambda s: get_supported_labels().index(s) + 1
  179. self.label_inc_library = __read_and_parse_events(input_file, l1, l2)
  180. else:
  181. raise ValueError(f"Unknown section code: {section}")
  182. input_file.close() # Close file
  183. if version_combined < 1002000:
  184. raise ValueError(
  185. f"Unsupported version {version_combined}, only file format revision 1.2.0 (1002000) and above "
  186. f"are supported."
  187. )
  188. # Fix blocks, gradients and RF objects imported from older versions
  189. if version_combined < 1004000:
  190. # Scan through RF objects
  191. for i in self.rf_library.data.keys():
  192. self.rf_library.data[i] = [
  193. *self.rf_library.data[i][:3],
  194. 0,
  195. *self.rf_library.data[i][3:],
  196. ]
  197. self.rf_library.lengths[i] += 1
  198. # Scan through the gradient objects and update 't'-s (trapezoids) und 'g'-s (free-shape gradients)
  199. for i in self.grad_library.data.keys():
  200. if self.grad_library.type[i] == "t":
  201. if self.grad_library.data[i][1] == 0:
  202. if (
  203. np.abs(self.grad_library.data[i][0]) == 0
  204. and self.grad_library.data[i][2] > 0
  205. ):
  206. self.grad_library.data[i][2] -= self.grad_raster_time
  207. self.grad_library.data[i][1] = self.grad_raster_time
  208. if self.grad_library.data[i][3] == 0:
  209. if (
  210. np.abs(self.grad_library.data[i][0]) == 0
  211. and self.grad_library.data[i][2] > 0
  212. ):
  213. self.grad_library.data[i][2] -= self.grad_raster_time
  214. self.grad_library.data[i][3] = self.grad_raster_time
  215. if self.grad_library.type[i] == "g":
  216. self.grad_library.data[i] = [
  217. self.grad_library.data[i][:2],
  218. 0,
  219. self.grad_library.data[i][2:],
  220. ]
  221. self.grad_library.lengths[i] += 1
  222. # For versions prior to 1.4.0 block_durations have not been initialized
  223. self.block_durations = np.zeros(len(self.block_events))
  224. # Scan through blocks and calculate durations
  225. for block_counter in range(len(self.block_events)):
  226. block = self.get_block(block_counter + 1)
  227. if delay_ind_temp[block_counter] > 0:
  228. block.delay = SimpleNamespace()
  229. block.delay.type = "delay"
  230. block.delay.delay = temp_delay_library.data[
  231. delay_ind_temp[block_counter]
  232. ]
  233. self.block_durations[block_counter] = calc_duration(block)
  234. grad_channels = ["gx", "gy", "gz"]
  235. grad_prev_last = np.zeros(len(grad_channels))
  236. for block_counter in range(len(self.block_events)):
  237. block = self.get_block(block_counter + 1)
  238. block_duration = block.block_duration
  239. # We also need to keep track of the event IDs because some PyPulseq files written by external software may contain
  240. # repeated entries so searching by content will fail
  241. event_idx = self.block_events[block_counter + 1]
  242. # Update the objects by filling in the fields not contained in the PyPulseq file
  243. for j in range(len(grad_channels)):
  244. grad = getattr(block, grad_channels[j])
  245. if grad is None:
  246. grad_prev_last[j] = 0
  247. continue
  248. if grad.type == "grad":
  249. if grad.delay > 0:
  250. grad_prev_last[j] = 0
  251. if hasattr(grad, "first"):
  252. continue
  253. grad.first = grad_prev_last[j]
  254. if grad.time_id != 0:
  255. grad.last = grad.waveform[-1]
  256. grad_duration = grad.delay + grad.tt[-1]
  257. else:
  258. # Restore samples on the edges of the gradient raster intervals for that we need the first sample
  259. odd_step1 = [grad.first, *2 * grad.waveform]
  260. odd_step2 = odd_step1 * (np.mod(range(len(odd_step1)), 2) * 2 - 1)
  261. waveform_odd_rest = np.cumsum(odd_step2) * (
  262. np.mod(len(odd_step2), 2) * 2 - 1
  263. )
  264. grad.last = waveform_odd_rest[-1]
  265. grad_duration = (
  266. grad.delay + len(grad.waveform) * self.grad_raster_time
  267. )
  268. # Bookkeeping
  269. grad_prev_last[j] = grad.last
  270. eps = np.finfo(np.float64).eps
  271. if grad_duration + eps < block_duration:
  272. grad_prev_last[j] = 0
  273. amplitude_ID = event_idx[j + 2]
  274. amplitude = self.grad_library.data[amplitude_ID][0]
  275. if version_combined >= 1004000:
  276. old_data = np.array(
  277. [amplitude, grad.shape_id, grad.time_id, grad.delay]
  278. )
  279. else:
  280. old_data = np.array([amplitude, grad.shape_id, grad.delay])
  281. new_data = np.array(
  282. [
  283. amplitude,
  284. grad.shape_id,
  285. grad.time_id,
  286. grad.delay,
  287. grad.first,
  288. grad.last,
  289. ]
  290. )
  291. self.grad_library.update_data(amplitude_ID, old_data, new_data, "g")
  292. else:
  293. grad_prev_last[j] = 0
  294. if detect_rf_use:
  295. # Find the RF pulses, list flip angles, and work around the current (rev 1.2.0) Pulseq file format limitation
  296. # that the RF pulse use is not stored in the file
  297. for k in self.rf_library.keys.keys():
  298. lib_data = self.rf_library.data[k]
  299. rf = self.rf_from_lib_data(lib_data)
  300. flip_deg = np.abs(np.sum(rf.signal[:-1] * (rf.t[1:] - rf.t[:-1]))) * 360
  301. offresonance_ppm = 1e6 * rf.freq_offset / self.system.B0 / self.system.gamma
  302. if (
  303. flip_deg < 90.01
  304. ): # Add 0.01 degree to account for rounding errors encountered in very short RF pulses
  305. self.rf_library.type[k] = "e"
  306. else:
  307. if (
  308. rf.shape_dur > 6e-3 and -3.5 <= offresonance_ppm <= -3.4
  309. ): # Approx -3.45
  310. self.rf_library.type[k] = "s" # Saturation (fat-sat)
  311. else:
  312. self.rf_library.type[k] = "r"
  313. self.rf_library.data[k] = lib_data
  314. def __read_definitions(input_file) -> Dict[str, str]:
  315. """
  316. Read the [DEFINITIONS] section of a sequence fil and return a map of key/value entries.
  317. Parameters
  318. ----------
  319. input_file : file object
  320. Sequence file.
  321. Returns
  322. -------
  323. definitions : dict{str, str}
  324. Dict object containing key value pairs of definitions.
  325. """
  326. definitions = dict()
  327. line = __skip_comments(input_file)
  328. while line != -1 and not (line == "" or line[0] == "#"):
  329. tok = line.split(" ")
  330. try: # Try converting every element into a float
  331. [float(x) for x in tok[1:]]
  332. value = np.array(tok[1:], dtype=float)
  333. if len(value) == 1: # Avoid array structure for single elements
  334. value = value[0]
  335. definitions[tok[0]] = value
  336. except ValueError: # Try clause did not work!
  337. definitions[tok[0]] = tok[1:]
  338. line = __strip_line(input_file)
  339. return definitions
  340. def __read_version(input_file) -> Tuple[int, int, int]:
  341. """
  342. Read the [VERSION] section of a sequence file.
  343. Parameters
  344. ----------
  345. input_file : file object
  346. Sequence file.
  347. Returns
  348. -------
  349. tuple
  350. Major, minor and revision number.
  351. """
  352. line = __strip_line(input_file)
  353. major, minor, revision = 0, 0, 0
  354. while line != "" and line[0] != "#":
  355. tok = line.split(" ")
  356. if tok[0] == "major":
  357. major = int(tok[1])
  358. elif tok[0] == "minor":
  359. minor = int(tok[1])
  360. elif tok[0] == "revision":
  361. if len(tok[1]) != 1: # Example: x.y.zpostN
  362. tok[1] = tok[1][0]
  363. revision = int(tok[1])
  364. else:
  365. raise RuntimeError(
  366. f"Incompatible version. Expected: {major}{minor}{revision}"
  367. )
  368. line = __strip_line(input_file)
  369. return major, minor, revision
  370. def __read_blocks(
  371. input_file, block_duration_raster: float, version_combined: int
  372. ) -> Tuple[Dict[int, np.ndarray], List[float], List[int]]:
  373. """
  374. Read the [BLOCKS] section of a sequence file and return the event table.
  375. Parameters
  376. ----------
  377. input_file : file
  378. Sequence file
  379. Returns
  380. -------
  381. event_table : dict
  382. Dict object containing key value pairs of Pulseq block ID and block definition.
  383. block_durations : list
  384. Block durations.
  385. delay_idx : list
  386. Delay IDs.
  387. """
  388. event_table = dict()
  389. block_durations = []
  390. delay_idx = []
  391. line = __strip_line(input_file)
  392. while line != "" and line != "#":
  393. block_events = np.fromstring(line, dtype=int, sep=" ")
  394. if version_combined <= 1002001:
  395. event_table[block_events[0]] = np.array([0, *block_events[2:], 0])
  396. else:
  397. event_table[block_events[0]] = np.array([0, *block_events[2:]])
  398. delay_id = block_events[0]
  399. if version_combined >= 1004000:
  400. if delay_id > len(block_durations):
  401. block_durations.append(block_events[1] * block_duration_raster)
  402. else:
  403. block_durations[delay_id] = block_events[1] * block_duration_raster
  404. else:
  405. if delay_id > len(delay_idx):
  406. delay_idx.append(block_events[1])
  407. else:
  408. delay_idx[delay_id] = block_events[1]
  409. line = __strip_line(input_file)
  410. return event_table, block_durations, delay_idx
  411. def __read_events(
  412. input_file,
  413. scale: tuple = (1,),
  414. event_type: str = str(),
  415. event_library: EventLibrary = EventLibrary(),
  416. ) -> EventLibrary:
  417. """
  418. Read an event section of a sequence file and return a library of events.
  419. Parameters
  420. ----------
  421. input_file : file object
  422. Sequence file.
  423. scale : list, default=(1,)
  424. Scale elements according to column vector scale.
  425. event_type : str, default=str()
  426. Attach the type string to elements of the library.
  427. event_library : EventLibrary, default=EventLibrary()
  428. Append new events to the given library.
  429. Returns
  430. -------
  431. event_library : EventLibrary
  432. Event library containing Pulseq events.
  433. """
  434. line = __strip_line(input_file)
  435. while line != "" and line != "#":
  436. data = np.fromstring(line, dtype=float, sep=" ")
  437. event_id = data[0]
  438. data = data[1:] * scale
  439. if event_type == "":
  440. event_library.insert(key_id=event_id, new_data=data)
  441. else:
  442. event_library.insert(key_id=event_id, new_data=data, data_type=event_type)
  443. line = __strip_line(input_file)
  444. return event_library
  445. def __read_and_parse_events(input_file, *args: callable) -> EventLibrary:
  446. """
  447. Read an event section of a sequence file and return a library of events. Event data elements are converted using
  448. the provided parser(s). Default parser is `int()`.
  449. Parameters
  450. ----------
  451. input_file : file
  452. args : callable
  453. Event parsers.
  454. Returns
  455. -------
  456. EventLibrary
  457. Library of events parsed from the events section of a sequence file.
  458. """
  459. event_library = EventLibrary()
  460. line = __strip_line(input_file)
  461. while line != "" and line != "#":
  462. datas = re.split("(\s+)", line)
  463. datas = [d for d in datas if d != " "]
  464. data = np.zeros(len(datas) - 1, dtype=np.int32)
  465. event_id = int(datas[0])
  466. for i in range(1, len(datas)):
  467. if i > len(args):
  468. data[i - 1] = int(datas[i])
  469. else:
  470. data[i - 1] = args[i - 1](datas[i])
  471. event_library.insert(key_id=event_id, new_data=data)
  472. line = __strip_line(input_file)
  473. return event_library
  474. def __read_shapes(input_file, force_convert_uncompressed: bool) -> EventLibrary:
  475. """
  476. Read the [SHAPES] section of a sequence file and return a library of shapes.
  477. Parameters
  478. ----------
  479. input_file : file
  480. Returns
  481. -------
  482. shape_library : EventLibrary
  483. `EventLibrary` object containing shape definitions.
  484. """
  485. shape_library = EventLibrary()
  486. line = __skip_comments(input_file)
  487. while line != -1 and (line != "" or line[0:8] == "shape_id"):
  488. tok = line.split(" ")
  489. shape_id = int(tok[1])
  490. line = __skip_comments(input_file)
  491. tok = line.split(" ")
  492. num_samples = int(tok[1])
  493. data = []
  494. line = __skip_comments(input_file)
  495. while line != "" and line != "#":
  496. data.append(float(line))
  497. line = __strip_line(input_file)
  498. line = __skip_comments(input_file, stop_before_section=True)
  499. # Check if conversion is needed: in v1.4.x we use length(data)==num_samples
  500. # As a marker for the uncompressed (stored) data. In older versions this condition could occur by chance
  501. if force_convert_uncompressed and len(data) == num_samples:
  502. shape = SimpleNamespace()
  503. shape.data = data
  504. shape.num_samples = num_samples
  505. shape = compress_shape(decompress_shape(shape, force_decompression=True))
  506. data = np.array([shape.num_samples, *shape.data])
  507. else:
  508. data.insert(0, num_samples)
  509. data = np.asarray(data)
  510. shape_library.insert(key_id=shape_id, new_data=data)
  511. return shape_library
  512. def __skip_comments(input_file, stop_before_section: bool = False) -> str:
  513. """
  514. Read lines of skipping blank lines and comments and return the next non-comment line.
  515. Parameters
  516. ----------
  517. input_file : file
  518. Returns
  519. -------
  520. line : str
  521. First line in `input_file` after skipping one '#' comment block. Note: File pointer is remembered, so
  522. successive calls work as expected.
  523. """
  524. temp_pos = input_file.tell()
  525. line = __strip_line(input_file)
  526. while line != -1 and (line == "" or line[0] == "#"):
  527. temp_pos = input_file.tell()
  528. line = __strip_line(input_file)
  529. if line != -1:
  530. if stop_before_section and line[0] == "[":
  531. input_file.seek(temp_pos, 0)
  532. next_line = ""
  533. else:
  534. next_line = line
  535. else:
  536. next_line = -1
  537. return next_line
  538. def __strip_line(input_file) -> str:
  539. """
  540. Removes spaces and newline whitespaces.
  541. Parameters
  542. ----------
  543. input_file : file
  544. Returns
  545. -------
  546. line : str
  547. First line in input_file after spaces and newline whitespaces have been removed. Note: File pointer is
  548. remembered, and hence successive calls work as expected. Returns -1 for eof.
  549. """
  550. line = (
  551. input_file.readline()
  552. ) # If line is an empty string, end of the file has been reached
  553. return line.strip() if line != "" else -1