sequence.py 76 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893
  1. import itertools
  2. import math
  3. from collections import OrderedDict
  4. from types import SimpleNamespace
  5. from typing import Tuple, List
  6. from typing import Union
  7. from warnings import warn
  8. import tkinter as tk
  9. try:
  10. from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
  11. from matplotlib.figure import Figure
  12. import matplotlib as mpl
  13. from matplotlib import pyplot as plt
  14. except Exception:
  15. FigureCanvasTkAgg = None
  16. NavigationToolbar2Tk = None
  17. Figure = None
  18. mpl = None
  19. plt = None
  20. try:
  21. from typing import Self
  22. except ImportError:
  23. from typing import TypeVar
  24. Self = TypeVar('Self', bound='Sequence')
  25. import numpy as np
  26. try:
  27. from scipy.interpolate import PPoly
  28. except Exception:
  29. PPoly = None
  30. from LF_scanner.pypulseq import eps
  31. from LF_scanner.pypulseq.calc_rf_center import calc_rf_center
  32. from LF_scanner.pypulseq.check_timing import check_timing as ext_check_timing
  33. from LF_scanner.pypulseq.decompress_shape import decompress_shape
  34. from LF_scanner.pypulseq.event_lib import EventLibrary
  35. from LF_scanner.pypulseq.opts import Opts
  36. from LF_scanner.pypulseq.supported_labels_rf_use import get_supported_labels
  37. from LF_scanner.version import major, minor, revision
  38. from LF_scanner.pypulseq.block_to_events import block_to_events
  39. from LF_scanner.pypulseq.utils.cumsum import cumsum
  40. from copy import deepcopy
  41. from LF_scanner.pypulseq.Sequence import block
  42. try:
  43. from LF_scanner.pypulseq.Sequence import parula
  44. except Exception:
  45. parula = None
  46. from LF_scanner.pypulseq.Sequence.ext_test_report import ext_test_report
  47. from LF_scanner.pypulseq.Sequence.read_seq import read
  48. from LF_scanner.pypulseq.Sequence.write_seq import write as write_seq
  49. try:
  50. from LF_scanner.pypulseq.Sequence.calc_pns import calc_pns
  51. except Exception:
  52. calc_pns = None
  53. try:
  54. from LF_scanner.pypulseq.Sequence.calc_grad_spectrum import calculate_gradient_spectrum
  55. except Exception:
  56. calculate_gradient_spectrum = None
  57. class Sequence:
  58. """
  59. Generate sequences and read/write sequence files. This class defines properties and methods to define a complete MR
  60. sequence including RF pulses, gradients, ADC events, etc. The class provides an implementation of the open MR
  61. sequence format defined by the Pulseq project. See http://pulseq.github.io/.
  62. See also `demo_read.py`, `demo_write.py`.
  63. """
  64. version_major = int(major)
  65. version_minor = int(minor)
  66. version_revision = revision
  67. def __init__(self, system=None, use_block_cache=True):
  68. if system == None:
  69. system = Opts()
  70. # =========
  71. # EVENT LIBRARIES
  72. # =========
  73. self.adc_library = EventLibrary() # Library of ADC events
  74. self.delay_library = EventLibrary() # Library of delay events
  75. # Library of extension events. Extension events form single-linked zero-terminated lists
  76. self.extensions_library = EventLibrary()
  77. self.grad_library = EventLibrary() # Library of gradient events
  78. self.label_inc_library = (
  79. EventLibrary()
  80. ) # Library of Label(inc) events (reference from the extensions library)
  81. self.label_set_library = (
  82. EventLibrary()
  83. ) # Library of Label(set) events (reference from the extensions library)
  84. self.rf_library = EventLibrary() # Library of RF events
  85. self.shape_library = EventLibrary(numpy_data=True) # Library of compressed shapes
  86. self.trigger_library = EventLibrary() # Library of trigger events
  87. # =========
  88. # OTHER
  89. # =========
  90. self.system = system
  91. self.block_events = OrderedDict() # Event table
  92. self.use_block_cache = use_block_cache
  93. self.block_cache = dict() # Block cache
  94. self.next_free_block_ID = 1
  95. self.definitions = dict() # Optional sequence definitions
  96. self.rf_raster_time = (
  97. self.system.rf_raster_time
  98. ) # RF raster time (system dependent)
  99. self.grad_raster_time = (
  100. self.system.grad_raster_time
  101. ) # Gradient raster time (system dependent)
  102. self.adc_raster_time = (
  103. self.system.adc_raster_time
  104. ) # ADC raster time (system dependent)
  105. self.block_duration_raster = self.system.block_duration_raster
  106. self.set_definition("AdcRasterTime", self.adc_raster_time)
  107. self.set_definition("BlockDurationRaster", self.block_duration_raster)
  108. self.set_definition("GradientRasterTime", self.grad_raster_time)
  109. self.set_definition("RadiofrequencyRasterTime", self.rf_raster_time)
  110. self.signature_type = ""
  111. self.signature_file = ""
  112. self.signature_value = ""
  113. self.block_durations = dict() # Cache of block durations
  114. self.extension_numeric_idx = [] # numeric IDs of the used extensions
  115. self.extension_string_idx = [] # string IDs of the used extensions
  116. def __str__(self) -> str:
  117. s = "Sequence:"
  118. s += "\nshape_library: " + str(self.shape_library)
  119. s += "\nrf_library: " + str(self.rf_library)
  120. s += "\ngrad_library: " + str(self.grad_library)
  121. s += "\nadc_library: " + str(self.adc_library)
  122. s += "\ndelay_library: " + str(self.delay_library)
  123. s += "\nextensions_library: " + str(
  124. self.extensions_library
  125. ) # inserted for trigger support by mveldmann
  126. s += "\nrf_raster_time: " + str(self.rf_raster_time)
  127. s += "\ngrad_raster_time: " + str(self.grad_raster_time)
  128. s += "\nblock_events: " + str(len(self.block_events))
  129. return s
  130. def adc_times(
  131. self, time_range: List[float] = None
  132. ) -> Tuple[np.ndarray, np.ndarray]:
  133. """
  134. Return time points of ADC sampling points.
  135. Returns
  136. -------
  137. t_adc: np.ndarray
  138. Contains times of all ADC sample points.
  139. fp_adc : np.ndarray
  140. Contains frequency and phase offsets of each ADC object (not samples).
  141. """
  142. # Collect ADC timing data
  143. t_adc = []
  144. fp_adc = []
  145. curr_dur = 0
  146. if time_range == None:
  147. blocks = self.block_events
  148. else:
  149. if len(time_range) != 2:
  150. raise ValueError('Time range must be list of two elements')
  151. if time_range[0] > time_range[1]:
  152. raise ValueError('End time of time_range must be after begin time')
  153. # Calculate end times of each block
  154. bd = np.array(list(self.block_durations.values()))
  155. t = np.cumsum(bd)
  156. # Search block end times for start of time range
  157. begin_block = np.searchsorted(t, time_range[0])
  158. # Search block begin times for end of time range
  159. end_block = np.searchsorted(t - bd, time_range[1], side='right')
  160. blocks = list(self.block_durations.keys())[begin_block:end_block]
  161. curr_dur = t[begin_block] - bd[begin_block]
  162. for block_counter in blocks:
  163. block = self.get_block(block_counter)
  164. if block.adc is not None: # ADC
  165. t_adc.append(
  166. (np.arange(block.adc.num_samples) + 0.5) * block.adc.dwell
  167. + block.adc.delay
  168. + curr_dur
  169. )
  170. fp_adc.append([block.adc.freq_offset, block.adc.phase_offset])
  171. curr_dur += self.block_durations[block_counter]
  172. if t_adc == []:
  173. # If there are no ADCs, make sure the output is the right shape
  174. t_adc = np.zeros(0)
  175. fp_adc = np.zeros((0, 2))
  176. else:
  177. t_adc = np.concatenate(t_adc)
  178. fp_adc = np.array(fp_adc)
  179. return t_adc, fp_adc
  180. def add_block(self, *args: SimpleNamespace) -> None:
  181. """
  182. Add a new block/multiple events to the sequence. Adds a sequence block with provided as a block structure
  183. See also:
  184. - `pypulseq.Sequence.sequence.Sequence.set_block()`
  185. - `pypulseq.make_adc.make_adc()`
  186. - `pypulseq.make_trapezoid.make_trapezoid()`
  187. - `pypulseq.make_sinc_pulse.make_sinc_pulse()`
  188. Parameters
  189. ----------
  190. args : SimpleNamespace
  191. Block structure or events to be added as a block to `Sequence`.
  192. """
  193. block.set_block(self, self.next_free_block_ID, *args)
  194. self.next_free_block_ID += 1
  195. def calculate_gradient_spectrum(
  196. self, max_frequency: float = 2000,
  197. window_width: float = 0.05,
  198. frequency_oversampling: float = 3,
  199. time_range: List[float] = None,
  200. plot: bool = True,
  201. combine_mode: str = 'max',
  202. use_derivative: bool = False,
  203. acoustic_resonances: List[dict] = []
  204. ) -> Tuple[List[np.ndarray], np.ndarray, np.ndarray, np.ndarray]:
  205. """
  206. Calculates the gradient spectrum of the sequence. Returns a spectrogram
  207. for each gradient channel, as well as a root-sum-squares combined
  208. spectrogram.
  209. Works by splitting the sequence into windows that are 'window_width'
  210. long and calculating the fourier transform of each window. Windows
  211. overlap 50% with the previous and next window. When 'combine_mode' is
  212. not 'none', all windows are combined into one spectrogram.
  213. Parameters
  214. ----------
  215. max_frequency : float, optional
  216. Maximum frequency to include in spectrograms. The default is 2000.
  217. window_width : float, optional
  218. Window width (in seconds). The default is 0.05.
  219. frequency_oversampling : float, optional
  220. Oversampling in the frequency dimension, higher values make
  221. smoother spectrograms. The default is 3.
  222. time_range : List[float], optional
  223. Time range over which to calculate the spectrograms as a list of
  224. two timepoints (in seconds) (e.g. [1, 1.5])
  225. The default is None.
  226. plot : bool, optional
  227. Whether to plot the spectograms. The default is True.
  228. combine_mode : str, optional
  229. How to combine all windows into one spectrogram, options:
  230. 'max', 'mean', 'sos' (root-sum-of-squares), 'none' (no combination)
  231. The default is 'max'.
  232. use_derivative : bool, optional
  233. Whether the use the derivative of the gradient waveforms instead of the
  234. gradient waveforms for the gradient spectrum calculations. The default
  235. is False
  236. acoustic_resonances : List[dict], optional
  237. Acoustic resonances as a list of dictionaries with 'frequency' and
  238. 'bandwidth' elements. Only used when plot==True. The default is [].
  239. Returns
  240. -------
  241. spectrograms : List[np.ndarray]
  242. List of spectrograms per gradient channel.
  243. spectrogram_sos : np.ndarray
  244. Root-sum-of-squares combined spectrogram over all gradient channels.
  245. frequencies : np.ndarray
  246. Frequency axis of the spectrograms.
  247. times : np.ndarray
  248. Time axis of the spectrograms (only relevant when combine_mode == 'none').
  249. """
  250. return calculate_gradient_spectrum(self, max_frequency=max_frequency,
  251. window_width=window_width,
  252. frequency_oversampling=frequency_oversampling,
  253. time_range=time_range,
  254. plot=plot,
  255. combine_mode=combine_mode,
  256. use_derivative=use_derivative,
  257. acoustic_resonances=acoustic_resonances)
  258. def calculate_kspace(
  259. self,
  260. trajectory_delay: Union[float, List[float], np.ndarray] = 0,
  261. gradient_offset: Union[float, List[float], np.ndarray] = 0
  262. ) -> Tuple[np.array, np.array, List[float], List[float], np.array]:
  263. """
  264. Calculates the k-space trajectory of the entire pulse sequence.
  265. Parameters
  266. ----------
  267. trajectory_delay : float or list, default=0
  268. Compensation factor in seconds (s) to align ADC and gradients in the reconstruction.
  269. gradient_offset : float or list, default=0
  270. Simulates background gradients (specified in Hz/m)
  271. Returns
  272. -------
  273. k_traj_adc : numpy.array
  274. K-space trajectory sampled at `t_adc` timepoints.
  275. k_traj : numpy.array
  276. K-space trajectory of the entire pulse sequence.
  277. t_excitation : List[float]
  278. Excitation timepoints.
  279. t_refocusing : List[float]
  280. Refocusing timepoints.
  281. t_adc : numpy.array
  282. Sampling timepoints.
  283. """
  284. if np.any(np.abs(trajectory_delay) > 100e-6):
  285. raise Warning(
  286. f"Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high"
  287. )
  288. total_duration = sum(self.block_durations.values())
  289. t_excitation, fp_excitation, t_refocusing, _ = self.rf_times()
  290. t_adc, _ = self.adc_times()
  291. # Convert data to piecewise polynomials
  292. gw_pp = self.get_gradients(trajectory_delay, gradient_offset)
  293. ng = len(gw_pp)
  294. # Calculate slice positions. For now we entirely rely on the excitation -- ignoring complicated interleaved
  295. # refocused sequences
  296. if len(t_excitation) > 0:
  297. # Position in x, y, z
  298. slice_pos = np.zeros((ng, len(t_excitation)))
  299. for j in range(ng):
  300. if gw_pp[j] is None:
  301. slice_pos[j] = np.nan
  302. else:
  303. # Check for divisions by zero to avoid numpy warning
  304. divisor = np.array(gw_pp[j](t_excitation))
  305. slice_pos[j, divisor != 0.0] = fp_excitation[0, divisor != 0.0] / divisor[divisor != 0.0]
  306. slice_pos[j, divisor == 0.0] = np.nan
  307. slice_pos[~np.isfinite(slice_pos)] = 0 # Reset undefined to 0
  308. else:
  309. slice_pos = []
  310. # =========
  311. # Integrate waveforms as PPs to produce gradient moments
  312. gm_pp = []
  313. tc = []
  314. for i in range(ng):
  315. if gw_pp[i] is None:
  316. gm_pp.append(None)
  317. continue
  318. gm_pp.append(gw_pp[i].antiderivative())
  319. tc.append(gm_pp[i].x)
  320. # "Sample" ramps for display purposes otherwise piecewise-linear display (plot) fails
  321. ii = np.flatnonzero(np.abs(gm_pp[i].c[0, :]) > eps)
  322. # Do nothing if there are no ramps
  323. if ii.shape[0] == 0:
  324. continue
  325. starts = np.int64(np.floor((gm_pp[i].x[ii] + eps) / self.grad_raster_time))
  326. ends = np.int64(np.ceil((gm_pp[i].x[ii + 1] - eps) / self.grad_raster_time))
  327. # Create all ranges starts[0]:ends[0], starts[1]:ends[1], etc.
  328. lengths = ends - starts + 1
  329. inds = np.ones((lengths).sum())
  330. # Calculate output index where each range will start
  331. start_inds = np.cumsum(np.concatenate(([0], lengths[:-1])))
  332. # Create element-wise differences that will cumsum into
  333. # the final indices: [starts[0], 1, 1, starts[1]-starts[0]-lengths[0]+1, 1, etc.]
  334. inds[start_inds] = np.concatenate(([starts[0]], np.diff(starts) - lengths[:-1] + 1))
  335. tc.append(np.cumsum(inds) * self.grad_raster_time)
  336. if tc != []:
  337. tc = np.concatenate(tc)
  338. t_acc = 1e-10 # Temporal accuracy
  339. t_acc_inv = 1 / t_acc
  340. # tc = self.__flatten_jagged_arr(tc)
  341. t_ktraj = t_acc * np.unique(
  342. np.round(
  343. t_acc_inv
  344. * np.array(
  345. [
  346. *tc,
  347. 0,
  348. *np.asarray(t_excitation) - 2 * self.rf_raster_time,
  349. *np.asarray(t_excitation) - self.rf_raster_time,
  350. *t_excitation,
  351. *np.asarray(t_refocusing) - self.rf_raster_time,
  352. *t_refocusing,
  353. *t_adc,
  354. total_duration,
  355. ]
  356. )
  357. )
  358. )
  359. i_excitation = np.searchsorted(t_ktraj, t_acc * np.round(t_acc_inv * np.asarray(t_excitation)))
  360. i_refocusing = np.searchsorted(t_ktraj, t_acc * np.round(t_acc_inv * np.asarray(t_refocusing)))
  361. i_adc = np.searchsorted(t_ktraj, t_acc * np.round(t_acc_inv * np.asarray(t_adc)))
  362. i_periods = np.unique([0, *i_excitation, *i_refocusing, len(t_ktraj) - 1])
  363. if len(i_excitation) > 0:
  364. ii_next_excitation = 0
  365. else:
  366. ii_next_excitation = -1
  367. if len(i_refocusing) > 0:
  368. ii_next_refocusing = 0
  369. else:
  370. ii_next_refocusing = -1
  371. k_traj = np.zeros((ng, len(t_ktraj)))
  372. for i in range(ng):
  373. if gw_pp[i] is None:
  374. continue
  375. it = np.where(np.logical_and(
  376. t_ktraj >= t_acc * round(t_acc_inv * gm_pp[i].x[0]),
  377. t_ktraj <= t_acc * round(t_acc_inv * gm_pp[i].x[-1]),
  378. ))[0]
  379. k_traj[i, it] = gm_pp[i](t_ktraj[it])
  380. if t_ktraj[it[-1]] < t_ktraj[-1]:
  381. k_traj[i, it[-1] + 1:] = k_traj[i, it[-1]]
  382. # Convert gradient moments to kspace
  383. dk = -k_traj[:, 0]
  384. for i in range(len(i_periods) - 1):
  385. i_period = i_periods[i]
  386. i_period_end = i_periods[i + 1]
  387. if ii_next_excitation >= 0 and i_excitation[ii_next_excitation] == i_period:
  388. if abs(t_ktraj[i_period] - t_excitation[ii_next_excitation]) > t_acc:
  389. raise Warning(
  390. f"abs(t_ktraj[i_period]-t_excitation[ii_next_excitation]) < {t_acc} failed for ii_next_excitation={ii_next_excitation} error={t_ktraj(i_period) - t_excitation(ii_next_excitation)}"
  391. )
  392. dk = -k_traj[:, i_period]
  393. if i_period > 0:
  394. # Use nans to mark the excitation points since they interrupt the plots
  395. k_traj[:, i_period - 1] = np.nan
  396. # -1 on len(i_excitation) for 0-based indexing
  397. ii_next_excitation = min(len(i_excitation) - 1, ii_next_excitation + 1)
  398. elif (
  399. ii_next_refocusing >= 0 and i_refocusing[ii_next_refocusing] == i_period
  400. ):
  401. # dk = -k_traj[:, i_period]
  402. dk = -2 * k_traj[:, i_period] - dk
  403. # -1 on len(i_excitation) for 0-based indexing
  404. ii_next_refocusing = min(len(i_refocusing) - 1, ii_next_refocusing + 1)
  405. k_traj[:, i_period:i_period_end] = (
  406. k_traj[:, i_period:i_period_end] + dk[:, None]
  407. )
  408. k_traj[:, i_period_end] = k_traj[:, i_period_end] + dk
  409. k_traj_adc = k_traj[:, i_adc]
  410. return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc
  411. def calculate_kspacePP(
  412. self,
  413. trajectory_delay: Union[float, List[float], np.ndarray] = 0,
  414. gradient_offset: Union[float, List[float], np.ndarray] = 0
  415. ) -> Tuple[np.array, np.array, np.array, np.array, np.array]:
  416. warn('Sequence.calculate_kspacePP has been deprecated, use calculate_kspace instead', DeprecationWarning,
  417. stacklevel=2)
  418. return self.calculate_kspace(trajectory_delay, gradient_offset)
  419. def calculate_pns(
  420. self,
  421. hardware: SimpleNamespace,
  422. time_range: List[float] = None,
  423. do_plots: bool = True
  424. ) -> Tuple[bool, np.array, np.ndarray, np.array]:
  425. """
  426. Calculate PNS using safe model implementation by Szczepankiewicz and Witzel
  427. See http://github.com/filip-szczepankiewicz/safe_pns_prediction
  428. Returns pns levels due to respective axes (normalized to 1 and not to 100#)
  429. Parameters
  430. ----------
  431. hardware : SimpleNamespace
  432. Hardware specifications. See safe_example_hw() from
  433. the safe_pns_prediction package. Alternatively a text file
  434. in the .asc format (Siemens) can be passed, e.g. for Prisma
  435. it is MP_GPA_K2309_2250V_951A_AS82.asc (we leave it as an
  436. exercise to the interested user to find were these files
  437. can be acquired from)
  438. do_plots : bool, optional
  439. Plot the results from the PNS calculations. The default is True.
  440. Returns
  441. -------
  442. ok : bool
  443. Boolean flag indicating whether peak PNS is within acceptable limits
  444. pns_norm : numpy.array [N]
  445. PNS norm over all gradient channels, normalized to 1
  446. pns_components : numpy.array [Nx3]
  447. PNS levels per gradient channel
  448. t_pns : np.array [N]
  449. Time axis for the pns_norm and pns_components arrays
  450. """
  451. return calc_pns(self, hardware, time_range=time_range, do_plots=do_plots)
  452. def check_timing(self) -> Tuple[bool, List[str]]:
  453. """
  454. Checks timing of all blocks and objects in the sequence optionally returns the detailed error log. This
  455. function also modifies the sequence object by adding the field "TotalDuration" to sequence definitions.
  456. Returns
  457. -------
  458. is_ok : bool
  459. Boolean flag indicating timing errors.
  460. error_report : str
  461. Error report in case of timing errors.
  462. """
  463. error_report = []
  464. is_ok = True
  465. total_duration = 0
  466. for block_counter in self.block_events:
  467. block = self.get_block(block_counter)
  468. events = block_to_events(block)
  469. res, rep, duration = ext_check_timing(self.system, *events)
  470. is_ok = is_ok and res
  471. # Check the stored total block duration
  472. if abs(duration - self.block_durations[block_counter]) > eps:
  473. rep += "Inconsistency between the stored block duration and the duration of the block content"
  474. is_ok = False
  475. duration = self.block_durations[block_counter]
  476. # Check RF dead times
  477. if block.rf is not None:
  478. if block.rf.delay - block.rf.dead_time < -eps:
  479. rep += (
  480. f"Delay of {block.rf.delay * 1e6} us is smaller than the RF dead time "
  481. f"{block.rf.dead_time * 1e6} us"
  482. )
  483. is_ok = False
  484. if (
  485. block.rf.delay + block.rf.t[-1] + block.rf.ringdown_time - duration
  486. > eps
  487. ):
  488. rep += (
  489. f"Time between the end of the RF pulse at {block.rf.delay + block.rf.t[-1]} and the end "
  490. f"of the block at {duration * 1e6} us is shorter than rf_ringdown_time"
  491. )
  492. is_ok = False
  493. # Check ADC dead times
  494. if block.adc is not None:
  495. if block.adc.delay - self.system.adc_dead_time < -eps:
  496. rep += "adc.delay < system.adc_dead_time"
  497. is_ok = False
  498. if (
  499. block.adc.delay
  500. + block.adc.num_samples * block.adc.dwell
  501. + self.system.adc_dead_time
  502. - duration
  503. > eps
  504. ):
  505. rep += "adc: system.adc_dead_time (post-ADC) violation"
  506. is_ok = False
  507. # Update report
  508. if len(rep) != 0:
  509. error_report.append(f"Event: {block_counter} - {rep}\n")
  510. total_duration += duration
  511. # Check if all the gradients in the last block are ramped down properly
  512. if len(events) != 0 and all([isinstance(e, SimpleNamespace) for e in events]):
  513. for e in range(len(events)):
  514. if not isinstance(events[e], list) and events[e].type == "grad":
  515. if events[e].last != 0:
  516. error_report.append(
  517. f"Event: {list(self.block_events)[-1]} - Gradients do not ramp to 0 at the end of the sequence"
  518. )
  519. self.set_definition("TotalDuration", total_duration)
  520. return is_ok, error_report
  521. def duration(self) -> Tuple[int, int, np.ndarray]:
  522. """
  523. Returns the total duration of this sequence, and the total count of blocks and events.
  524. Returns
  525. -------
  526. duration : int
  527. Duration of this sequence in seconds (s).
  528. num_blocks : int
  529. Number of blocks in this sequence.
  530. event_count : np.ndarray
  531. Number of events in this sequence.
  532. """
  533. num_blocks = len(self.block_events)
  534. event_count = np.zeros(len(next(iter(self.block_events.values()))))
  535. duration = 0
  536. for block_counter in self.block_events:
  537. event_count += self.block_events[block_counter] > 0
  538. duration += self.block_durations[block_counter]
  539. return duration, num_blocks, event_count
  540. def evaluate_labels(
  541. self,
  542. init: dict = None,
  543. evolution: str = 'none'
  544. ) -> dict:
  545. """
  546. Evaluate label values of the entire sequence.
  547. When no evolution is given, returns the label values at the end of the
  548. sequence. Returns a dictionary with keys named after the labels used
  549. in the sequence. Only the keys corresponding to the labels actually
  550. used are created.
  551. E.g. labels['LIN'] == 4
  552. When evolution is given, labels are tracked through the sequence. See
  553. below for options for different types of evolutions. The resulting
  554. dictionary will contain arrays of the label values.
  555. E.g. labels['LIN'] == np.array([0,1,2,3,4])
  556. Initial values for the labels can be given with the 'init' parameter.
  557. Useful if evaluating labels block-by-block.
  558. Parameters
  559. ----------
  560. init : dict, optional
  561. Dictionary containing initial label values. The default is None.
  562. evolution : str, optional
  563. Flag to specify tracking of label evolutions.
  564. Must be one of: 'none', 'adc', 'label', 'blocks' (default = 'none')
  565. 'blocks': Return label values for all blocks.
  566. 'adc': Return label values only for blocks containing ADC events.
  567. 'label': Return label values only for blocks where labels are
  568. manipulated.
  569. Returns
  570. -------
  571. labels : dict
  572. Dictionary containing label values.
  573. If evolution == 'none', the dictionary values only contains the
  574. final label value.
  575. Otherwise, the dictionary values are arrays of label evolutions.
  576. Only the labels that are used in the sequence are created in the
  577. dictionary.
  578. """
  579. labels = init or dict()
  580. label_evolution = []
  581. # TODO: MATLAB implementation includes block_range parameter. But in
  582. # general we cannot assume linear block ordering. Could include
  583. # time_range like in other sequence functions. Or a blocks
  584. # parameter to specify which blocks to loop over?
  585. for block_counter in self.block_events:
  586. block = self.get_block(block_counter)
  587. if block.label is not None:
  588. # Current block has labels
  589. for lab in block.label.values():
  590. if lab.type == 'labelinc':
  591. # Increment label
  592. if lab.label not in labels:
  593. labels[lab.label] = 0
  594. labels[lab.label] += lab.value
  595. else:
  596. # Set label
  597. labels[lab.label] = lab.value
  598. if evolution == 'label':
  599. label_evolution.append(dict(labels))
  600. if evolution == 'blocks' or (evolution == 'adc' and block.adc is not None):
  601. label_evolution.append(dict(labels))
  602. # Convert evolutions into label dictionary
  603. if len(label_evolution) > 0:
  604. for lab in labels:
  605. labels[lab] = np.array([e[lab] if lab in e else 0 for e in label_evolution])
  606. return labels
  607. def flip_grad_axis(self, axis: str) -> None:
  608. """
  609. Invert all gradients along the corresponding axis/channel. The function acts on all gradient objects already
  610. added to the sequence object.
  611. Parameters
  612. ----------
  613. axis : str
  614. Gradients to invert or scale. Must be one of 'x', 'y' or 'z'.
  615. """
  616. self.mod_grad_axis(axis, modifier=-1)
  617. def get_block(self, block_index: int) -> SimpleNamespace:
  618. """
  619. Return a block of the sequence specified by the index. The block is created from the sequence data with all
  620. events and shapes decompressed.
  621. See also:
  622. - `pypulseq.Sequence.sequence.Sequence.set_block()`.
  623. - `pypulseq.Sequence.sequence.Sequence.add_block()`.
  624. Parameters
  625. ----------
  626. block_index : int
  627. Index of block to be retrieved from `Sequence`.
  628. Returns
  629. -------
  630. SimpleNamespace
  631. Event identified by `block_index`.
  632. """
  633. return block.get_block(self, block_index)
  634. def get_definition(self, key: str) -> str:
  635. """
  636. Return value of the definition specified by the key. These definitions can be added manually or read from the
  637. header of a sequence file defined in the sequence header. An empty array is returned if the key is not defined.
  638. See also `pypulseq.Sequence.sequence.Sequence.set_definition()`.
  639. Parameters
  640. ----------
  641. key : str
  642. Key of definition to retrieve.
  643. Returns
  644. -------
  645. str
  646. Definition identified by `key` if found, else returns ''.
  647. """
  648. if key in self.definitions:
  649. return self.definitions[key]
  650. else:
  651. return ""
  652. def get_extension_type_ID(self, extension_string: str) -> int:
  653. """
  654. Get numeric extension ID for `extension_string`. Will automatically create a new ID if unknown.
  655. Parameters
  656. ----------
  657. extension_string : str
  658. Given string extension ID.
  659. Returns
  660. -------
  661. extension_id : int
  662. Numeric ID for given string extension ID.
  663. """
  664. if extension_string not in self.extension_string_idx:
  665. if len(self.extension_numeric_idx) == 0:
  666. extension_id = 1
  667. else:
  668. extension_id = 1 + max(self.extension_numeric_idx)
  669. self.extension_numeric_idx.append(extension_id)
  670. self.extension_string_idx.append(extension_string)
  671. assert len(self.extension_numeric_idx) == len(self.extension_string_idx)
  672. else:
  673. num = self.extension_string_idx.index(extension_string)
  674. extension_id = self.extension_numeric_idx[num]
  675. return extension_id
  676. def get_extension_type_string(self, extension_id: int) -> str:
  677. """
  678. Get string extension ID for `extension_id`.
  679. Parameters
  680. ----------
  681. extension_id : int
  682. Given numeric extension ID.
  683. Returns
  684. -------
  685. extension_str : str
  686. String ID for the given numeric extension ID.
  687. Raises
  688. ------
  689. ValueError
  690. If given numeric extension ID is unknown.
  691. """
  692. if extension_id in self.extension_numeric_idx:
  693. num = self.extension_numeric_idx.index(extension_id)
  694. else:
  695. raise ValueError(
  696. f"Extension for the given ID - {extension_id} - is unknown."
  697. )
  698. extension_str = self.extension_string_idx[num]
  699. return extension_str
  700. def get_gradients(self,
  701. trajectory_delay: Union[float, List[float], np.ndarray] = 0,
  702. gradient_offset: Union[float, List[float], np.ndarray] = 0,
  703. time_range: List[float] = None) -> List[PPoly]:
  704. """
  705. Get all gradient waveforms of the sequence in a piecewise-polynomial
  706. format (scipy PPoly). Gradient values can be accessed easily at one or
  707. more timepoints using `gw_pp[channel](t)` (where t is a float, list of
  708. floats, or numpy array). Note that PPoly objects return nan for
  709. timepoints outside the waveform.
  710. Parameters
  711. ----------
  712. trajectory_delay : float or list, default=0
  713. Compensation factor in seconds (s) to align ADC and gradients in the reconstruction.
  714. gradient_offset : float or list, default=0
  715. Simulates background gradients (specified in Hz/m)
  716. Returns
  717. -------
  718. gw_pp : List[PPoly]
  719. List of gradient waveforms for each of the gradient channels,
  720. expressed as scipy PPoly objects.
  721. """
  722. if np.any(np.abs(trajectory_delay) > 100e-6):
  723. raise Warning(
  724. f"Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high"
  725. )
  726. total_duration = sum(self.block_durations.values())
  727. gw_data = self.waveforms(time_range=time_range)
  728. ng = len(gw_data)
  729. # Gradient delay handling
  730. if isinstance(trajectory_delay, (int, float)):
  731. gradient_delays = [trajectory_delay] * ng
  732. else:
  733. assert (len(trajectory_delay) == ng) # Need to have same number of gradient channels
  734. gradient_delays = [trajectory_delay] * ng
  735. # Gradient offset handling
  736. if isinstance(gradient_offset, (int, float)):
  737. gradient_offset = [gradient_offset] * ng
  738. else:
  739. assert (len(gradient_offset) == ng) # Need to have same number of gradient channels
  740. # Convert data to piecewise polynomials
  741. gw_pp = []
  742. for j in range(ng):
  743. wave_cnt = gw_data[j].shape[1]
  744. if wave_cnt == 0:
  745. if np.abs(gradient_offset[j]) <= eps:
  746. gw_pp.append(None)
  747. continue
  748. else:
  749. gw = np.array(([0, total_duration], [0, 0]))
  750. else:
  751. gw = gw_data[j]
  752. # Now gw contains the waveform from the current axis
  753. if np.abs(gradient_delays[j]) > eps:
  754. gw[0] = gw[0] - gradient_delays[j] # Anisotropic gradient delay support
  755. if not np.all(np.isfinite(gw)):
  756. raise Warning("Not all elements of the generated waveform are finite.")
  757. teps = 1e-12
  758. _temp1 = np.array(([gw[0, 0] - 2 * teps, gw[0, 0] - teps], [0, 0]))
  759. _temp2 = np.array(([gw[0, -1] + teps, gw[0, -1] + 2 * teps], [0, 0]))
  760. gw = np.hstack((_temp1, gw, _temp2))
  761. if np.abs(gradient_offset[j]) > eps:
  762. gw[1, :] += gradient_offset[j]
  763. gw[1][gw[1] == -0.0] = 0.0
  764. gw_pp.append(PPoly(np.stack((np.diff(gw[1]) / np.diff(gw[0]),
  765. gw[1][:-1])), gw[0], extrapolate=True))
  766. return gw_pp
  767. def mod_grad_axis(self, axis: str, modifier: int) -> None:
  768. """
  769. Invert or scale all gradients along the corresponding axis/channel. The function acts on all gradient objects
  770. already added to the sequence object.
  771. Parameters
  772. ----------
  773. axis : str
  774. Gradients to invert or scale. Must be one of 'x', 'y' or 'z'.
  775. modifier : int
  776. Scaling value.
  777. Raises
  778. ------
  779. ValueError
  780. If invalid `axis` is passed. Must be one of 'x', 'y','z'.
  781. RuntimeError
  782. If same gradient event is used on multiple axes.
  783. """
  784. if axis not in ["x", "y", "z"]:
  785. raise ValueError(
  786. f"Invalid axis. Must be one of 'x', 'y','z'. Passed: {axis}"
  787. )
  788. channel_num = ["x", "y", "z"].index(axis)
  789. other_channels = [0, 1, 2]
  790. other_channels.remove(channel_num)
  791. # Go through all event table entries and list gradient objects in the library
  792. all_grad_events = np.array(list(self.block_events.values()))
  793. all_grad_events = all_grad_events[:, 2:5]
  794. selected_events = np.unique(all_grad_events[:, channel_num])
  795. selected_events = selected_events[selected_events != 0]
  796. other_events = np.unique(all_grad_events[:, other_channels])
  797. if len(np.intersect1d(selected_events, other_events)) > 0:
  798. raise RuntimeError(
  799. "mod_grad_axis does not yet support the same gradient event used on multiple axes."
  800. )
  801. for i in range(len(selected_events)):
  802. self.grad_library.data[selected_events[i]][0] *= modifier
  803. if (
  804. self.grad_library.type[selected_events[i]] == "g"
  805. and self.grad_library.lengths[selected_events[i]] == 5
  806. ):
  807. # Need to update first and last fields
  808. self.grad_library.data[selected_events[i]][3] *= modifier
  809. self.grad_library.data[selected_events[i]][4] *= modifier
  810. def plot(
  811. self, tk_obj,
  812. label: str = str(),
  813. show_blocks: bool = False,
  814. save: bool = False,
  815. time_range=(0, np.inf),
  816. time_disp: str = "s",
  817. grad_disp: str = "kHz/m",
  818. plot_now: bool = True,
  819. tk_plot: bool = True
  820. ) -> None:
  821. """
  822. Plot `Sequence`.
  823. Parameters
  824. ----------
  825. label : str, defualt=str()
  826. Plot label values for ADC events: in this example for LIN and REP labels; other valid labes are accepted as
  827. a comma-separated list.
  828. save : bool, default=False
  829. Boolean flag indicating if plots should be saved. The two figures will be saved as JPG with numerical
  830. suffixes to the filename 'seq_plot'.
  831. show_blocks : bool, default=False
  832. Boolean flag to indicate if grid and tick labels at the block boundaries are to be plotted.
  833. time_range : iterable, default=(0, np.inf)
  834. Time range (x-axis limits) for plotting the sequence. Default is 0 to infinity (entire sequence).
  835. time_disp : str, default='s'
  836. Time display type, must be one of `s`, `ms` or `us`.
  837. grad_disp : str, default='s'
  838. Gradient display unit, must be one of `kHz/m` or `mT/m`.
  839. plot_now : bool, default=True
  840. If true, function immediately shows the plots, blocking the rest of the code until plots are exited.
  841. If false, plots are shown when plt.show() is called. Useful if plots are to be modified.
  842. plot_type : str, default='Gradient'
  843. Gradients display type, must be one of either 'Gradient' or 'Kspace'.
  844. """
  845. mpl.rcParams["lines.linewidth"] = 0.75 # Set default Matplotlib linewidth
  846. valid_time_units = ["s", "ms", "us"]
  847. valid_grad_units = ["kHz/m", "mT/m"]
  848. valid_labels = get_supported_labels()
  849. if (
  850. not all([isinstance(x, (int, float)) for x in time_range])
  851. or len(time_range) != 2
  852. ):
  853. raise ValueError("Invalid time range")
  854. if time_disp not in valid_time_units:
  855. raise ValueError("Unsupported time unit")
  856. if grad_disp not in valid_grad_units:
  857. raise ValueError(
  858. "Unsupported gradient unit. Supported gradient units are: "
  859. + str(valid_grad_units)
  860. )
  861. fig1, fig2 = plt.figure(1), plt.figure(2)
  862. sp11 = fig1.add_subplot(311)
  863. sp12 = fig1.add_subplot(312, sharex=sp11)
  864. sp13 = fig1.add_subplot(313, sharex=sp11)
  865. fig2_subplots = [
  866. fig2.add_subplot(311, sharex=sp11),
  867. fig2.add_subplot(312, sharex=sp11),
  868. fig2.add_subplot(313, sharex=sp11),
  869. ]
  870. t_factor_list = [1, 1e3, 1e6]
  871. t_factor = t_factor_list[valid_time_units.index(time_disp)]
  872. g_factor_list = [1e-3, 1e3 / self.system.gamma]
  873. g_factor = g_factor_list[valid_grad_units.index(grad_disp)]
  874. t0 = 0
  875. label_defined = False
  876. label_idx_to_plot = []
  877. label_legend_to_plot = []
  878. label_store = dict()
  879. for i in range(len(valid_labels)):
  880. label_store[valid_labels[i]] = 0
  881. if valid_labels[i] in label.upper():
  882. label_idx_to_plot.append(i)
  883. label_legend_to_plot.append(valid_labels[i])
  884. if len(label_idx_to_plot) != 0:
  885. p = parula.main(len(label_idx_to_plot) + 1)
  886. label_colors_to_plot = p(np.arange(len(label_idx_to_plot)))
  887. cycler = mpl.cycler(color=label_colors_to_plot)
  888. sp11.set_prop_cycle(cycler)
  889. # Block timings
  890. block_edges = np.cumsum([0] + [x[1] for x in sorted(self.block_durations.items())])
  891. block_edges_in_range = block_edges[
  892. (block_edges >= time_range[0]) * (block_edges <= time_range[1])
  893. ]
  894. if show_blocks:
  895. for sp in [sp11, sp12, sp13, *fig2_subplots]:
  896. sp.set_xticks(t_factor * block_edges_in_range)
  897. sp.set_xticklabels(sp.get_xticklabels(), rotation=90)
  898. for block_counter in self.block_events:
  899. block = self.get_block(block_counter)
  900. is_valid = (time_range[0] <= t0 + self.block_durations[block_counter]
  901. and t0 <= time_range[1])
  902. if is_valid:
  903. if getattr(block, "label", None) is not None:
  904. for i in range(len(block.label)):
  905. if block.label[i].type == "labelinc":
  906. label_store[block.label[i].label] += block.label[i].value
  907. else:
  908. label_store[block.label[i].label] = block.label[i].value
  909. label_defined = True
  910. if getattr(block, "adc", None) is not None: # ADC
  911. adc = block.adc
  912. # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this
  913. # is the present convention - the samples are shifted by 0.5 dwell
  914. t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell
  915. sp11.plot(t_factor * (t0 + t), np.zeros(len(t)), "rx")
  916. sp13.plot(
  917. t_factor * (t0 + t),
  918. np.angle(
  919. np.exp(1j * adc.phase_offset)
  920. * np.exp(1j * 2 * np.pi * t * adc.freq_offset)
  921. ),
  922. "b.",
  923. markersize=0.25,
  924. )
  925. if label_defined and len(label_idx_to_plot) != 0:
  926. arr_label_store = list(label_store.values())
  927. lbl_vals = np.take(arr_label_store, label_idx_to_plot)
  928. t = t0 + adc.delay + (adc.num_samples - 1) / 2 * adc.dwell
  929. _t = [t_factor * t] * len(lbl_vals)
  930. # Plot each label individually to retrieve each corresponding Line2D object
  931. p = itertools.chain.from_iterable(
  932. [
  933. sp11.plot(__t, _lbl_vals, ".")
  934. for __t, _lbl_vals in zip(_t, lbl_vals)
  935. ]
  936. )
  937. if len(label_legend_to_plot) != 0:
  938. sp11.legend(p, label_legend_to_plot, loc="upper left")
  939. label_legend_to_plot = []
  940. if getattr(block, "rf", None) is not None: # RF
  941. rf = block.rf
  942. tc, ic = calc_rf_center(rf)
  943. time = rf.t
  944. signal = rf.signal
  945. if abs(signal[0]) != 0:
  946. signal = np.concatenate(([0], signal))
  947. time = np.concatenate(([time[0]], time))
  948. ic += 1
  949. if abs(signal[-1]) != 0:
  950. signal = np.concatenate((signal, [0]))
  951. time = np.concatenate((time, [time[-1]]))
  952. sp12.plot(t_factor * (t0 + time + rf.delay), np.abs(signal))
  953. sp13.plot(
  954. t_factor * (t0 + time + rf.delay),
  955. np.angle(
  956. signal
  957. * np.exp(1j * rf.phase_offset)
  958. * np.exp(1j * 2 * math.pi * time * rf.freq_offset)
  959. ),
  960. t_factor * (t0 + tc + rf.delay),
  961. np.angle(
  962. signal[ic]
  963. * np.exp(1j * rf.phase_offset)
  964. * np.exp(1j * 2 * math.pi * time[ic] * rf.freq_offset)
  965. ),
  966. "xb",
  967. )
  968. grad_channels = ["gx", "gy", "gz"]
  969. for x in range(len(grad_channels)): # Gradients
  970. if getattr(block, grad_channels[x], None) is not None:
  971. grad = getattr(block, grad_channels[x])
  972. if grad.type == "grad":
  973. # We extend the shape by adding the first and the last points in an effort of making the
  974. # display a bit less confusing...
  975. time = grad.delay + np.array([0, *grad.tt, grad.shape_dur])
  976. waveform = g_factor * np.array(
  977. (grad.first, *grad.waveform, grad.last)
  978. )
  979. else:
  980. time = np.array(cumsum(
  981. 0,
  982. grad.delay,
  983. grad.rise_time,
  984. grad.flat_time,
  985. grad.fall_time,
  986. ))
  987. waveform = (
  988. g_factor * grad.amplitude * np.array([0, 0, 1, 1, 0])
  989. )
  990. fig2_subplots[x].plot(t_factor * (t0 + time), waveform)
  991. t0 += self.block_durations[block_counter]
  992. grad_plot_labels = ["x", "y", "z"]
  993. sp11.set_ylabel("ADC")
  994. sp12.set_ylabel("RF mag (Hz)")
  995. sp13.set_ylabel("RF/ADC phase (rad)")
  996. sp13.set_xlabel(f"t ({time_disp})")
  997. for x in range(3):
  998. _label = grad_plot_labels[x]
  999. fig2_subplots[x].set_ylabel(f"G{_label} ({grad_disp})")
  1000. fig2_subplots[-1].set_xlabel(f"t ({time_disp})")
  1001. # Setting display limits
  1002. disp_range = t_factor * np.array([time_range[0], min(t0, time_range[1])])
  1003. [x.set_xlim(disp_range) for x in [sp11, sp12, sp13, *fig2_subplots]]
  1004. # Grid on
  1005. for sp in [sp11, sp12, sp13, *fig2_subplots]:
  1006. sp.grid()
  1007. fig1.tight_layout()
  1008. fig2.tight_layout()
  1009. if save:
  1010. fig1.savefig("seq_plot1.jpg")
  1011. fig2.savefig("seq_plot2.jpg")
  1012. if tk_plot:
  1013. root = tk.Tk()
  1014. root.title("График с панелью инструментов")
  1015. canvas = FigureCanvasTkAgg(fig1, master=root)
  1016. canvas.draw()
  1017. # Добавляем панель инструментов (зумирование, сохранение и т. д.)
  1018. toolbar = NavigationToolbar2Tk(canvas, root)
  1019. toolbar.update()
  1020. canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
  1021. if plot_now:
  1022. plt.show()
  1023. def read(self, file_path: str, detect_rf_use: bool = False, remove_duplicates: bool = True) -> None:
  1024. """
  1025. Read `.seq` file from `file_path`.
  1026. Parameters
  1027. ----------
  1028. detect_rf_use
  1029. file_path : str
  1030. Path to `.seq` file to be read.
  1031. remove_duplicates : bool, default=True
  1032. Remove duplicate events from the sequence after reading.
  1033. """
  1034. if self.use_block_cache:
  1035. self.block_cache.clear()
  1036. read(self, path=file_path, detect_rf_use=detect_rf_use, remove_duplicates=remove_duplicates)
  1037. # Initialize next free block ID
  1038. self.next_free_block_ID = (max(self.block_events) + 1) if self.block_events else 1
  1039. def register_adc_event(self, event: EventLibrary) -> int:
  1040. return block.register_adc_event(self, event)
  1041. def register_grad_event(
  1042. self, event: SimpleNamespace
  1043. ) -> Union[int, Tuple[int, int]]:
  1044. return block.register_grad_event(self, event)
  1045. def register_label_event(self, event: SimpleNamespace) -> int:
  1046. return block.register_label_event(self, event)
  1047. def register_rf_event(self, event: SimpleNamespace) -> Tuple[int, List[int]]:
  1048. return block.register_rf_event(self, event)
  1049. def remove_duplicates(self, in_place: bool = False) -> Self:
  1050. """
  1051. Removes duplicate events from the shape and event libraries contained
  1052. in this sequence.
  1053. Parameters
  1054. ----------
  1055. in_place : bool, optional
  1056. If true, removes the duplicates from the current sequence.
  1057. Otherwise, a copy is created. The default is False.
  1058. Returns
  1059. -------
  1060. seq_copy : Sequence
  1061. If `in_place`, returns self. Otherwise returns a copy of the
  1062. sequence.
  1063. """
  1064. if in_place:
  1065. seq_copy = self
  1066. else:
  1067. # Avoid copying block_cache for performance
  1068. tmp = self.block_cache
  1069. self.block_cache = {}
  1070. seq_copy = deepcopy(self)
  1071. self.block_cache = tmp
  1072. # Find duplicate in shape library
  1073. seq_copy.shape_library, mapping = seq_copy.shape_library.remove_duplicates(9)
  1074. # Remap shape IDs of arbitrary gradient events
  1075. for grad_id in seq_copy.grad_library.data:
  1076. if seq_copy.grad_library.type[grad_id] == 'g':
  1077. data = seq_copy.grad_library.data[grad_id]
  1078. new_data = (data[0],) + (mapping[data[1]], mapping[data[2]]) + data[3:]
  1079. if data != new_data:
  1080. seq_copy.grad_library.update(grad_id, None, new_data)
  1081. # Remap shape IDs of RF events
  1082. for rf_id in seq_copy.rf_library.data:
  1083. data = seq_copy.rf_library.data[rf_id]
  1084. new_data = (data[0],) + (mapping[data[1]], mapping[data[2]], mapping[data[3]]) + data[4:]
  1085. if data != new_data:
  1086. seq_copy.rf_library.update(rf_id, None, new_data)
  1087. # Filter duplicates in gradient library
  1088. seq_copy.grad_library, mapping = seq_copy.grad_library.remove_duplicates((6, -6, -6, -6, -6, -6))
  1089. # Remap gradient event IDs
  1090. for block_id in seq_copy.block_events:
  1091. seq_copy.block_events[block_id][2] = mapping[seq_copy.block_events[block_id][2]]
  1092. seq_copy.block_events[block_id][3] = mapping[seq_copy.block_events[block_id][3]]
  1093. seq_copy.block_events[block_id][4] = mapping[seq_copy.block_events[block_id][4]]
  1094. # Filter duplicates in RF library
  1095. seq_copy.rf_library, mapping = seq_copy.rf_library.remove_duplicates((6, 0, 0, 0, 6, 6, 6))
  1096. # Remap RF event IDs
  1097. for block_id in seq_copy.block_events:
  1098. seq_copy.block_events[block_id][1] = mapping[seq_copy.block_events[block_id][1]]
  1099. # Filter duplicates in ADC library
  1100. seq_copy.adc_library, mapping = seq_copy.adc_library.remove_duplicates((0, -9, -6, 6, 6, 6))
  1101. # Remap ADC event IDs
  1102. for block_id in seq_copy.block_events:
  1103. seq_copy.block_events[block_id][5] = mapping[seq_copy.block_events[block_id][5]]
  1104. return seq_copy
  1105. def rf_from_lib_data(self, lib_data: list, use: str = str()) -> SimpleNamespace:
  1106. """
  1107. Construct RF object from `lib_data`.
  1108. Parameters
  1109. ----------
  1110. lib_data : list
  1111. RF envelope.
  1112. use : str, default=str()
  1113. RF event use.
  1114. Returns
  1115. -------
  1116. rf : SimpleNamespace
  1117. RF object constructed from `lib_data`.
  1118. """
  1119. rf = SimpleNamespace()
  1120. rf.type = "rf"
  1121. amplitude, mag_shape, phase_shape = lib_data[0], lib_data[1], lib_data[2]
  1122. shape_data = self.shape_library.data[mag_shape]
  1123. compressed = SimpleNamespace()
  1124. compressed.num_samples = shape_data[0]
  1125. compressed.data = shape_data[1:]
  1126. mag = decompress_shape(compressed)
  1127. shape_data = self.shape_library.data[phase_shape]
  1128. compressed.num_samples = shape_data[0]
  1129. compressed.data = shape_data[1:]
  1130. phase = decompress_shape(compressed)
  1131. rf.signal = amplitude * mag * np.exp(1j * 2 * np.pi * phase)
  1132. time_shape = lib_data[3]
  1133. if time_shape > 0:
  1134. shape_data = self.shape_library.data[time_shape]
  1135. compressed.num_samples = shape_data[0]
  1136. compressed.data = shape_data[1:]
  1137. rf.t = decompress_shape(compressed) * self.rf_raster_time
  1138. rf.shape_dur = (
  1139. math.ceil((rf.t[-1] - eps) / self.rf_raster_time) * self.rf_raster_time
  1140. )
  1141. else: # Generate default time raster on the fly
  1142. rf.t = (np.arange(1, len(rf.signal) + 1) - 0.5) * self.rf_raster_time
  1143. rf.shape_dur = len(rf.signal) * self.rf_raster_time
  1144. rf.delay = lib_data[4]
  1145. rf.freq_offset = lib_data[5]
  1146. rf.phase_offset = lib_data[6]
  1147. rf.dead_time = self.system.rf_dead_time
  1148. rf.ringdown_time = self.system.rf_ringdown_time
  1149. if use != "":
  1150. use_cases = {
  1151. "e": "excitation",
  1152. "r": "refocusing",
  1153. "i": "inversion",
  1154. "s": "saturation",
  1155. "p": "preparation",
  1156. }
  1157. rf.use = use_cases[use] if use in use_cases else "undefined"
  1158. return rf
  1159. def rf_times(
  1160. self, time_range: List[float] = None
  1161. ) -> Tuple[List[float], np.ndarray, List[float], np.ndarray, np.ndarray]:
  1162. """
  1163. Return time points of excitations and refocusings.
  1164. Returns
  1165. -------
  1166. t_excitation : List[float]
  1167. Contains time moments of the excitation RF pulses
  1168. fp_excitation : np.ndarray
  1169. Contains frequency and phase offsets of the excitation RF pulses
  1170. t_refocusing : List[float]
  1171. Contains time moments of the refocusing RF pulses
  1172. fp_refocusing : np.ndarray
  1173. Contains frequency and phase offsets of the excitation RF pulses
  1174. """
  1175. # Collect RF timing data
  1176. t_excitation = []
  1177. fp_excitation = []
  1178. t_refocusing = []
  1179. fp_refocusing = []
  1180. curr_dur = 0
  1181. if time_range == None:
  1182. blocks = self.block_events
  1183. else:
  1184. if len(time_range) != 2:
  1185. raise ValueError('Time range must be list of two elements')
  1186. if time_range[0] > time_range[1]:
  1187. raise ValueError('End time of time_range must be after begin time')
  1188. # Calculate end times of each block
  1189. bd = np.array(list(self.block_durations.values()))
  1190. t = np.cumsum(bd)
  1191. # Search block end times for start of time range
  1192. begin_block = np.searchsorted(t, time_range[0])
  1193. # Search block begin times for end of time range
  1194. end_block = np.searchsorted(t - bd, time_range[1], side='right')
  1195. blocks = list(self.block_durations.keys())[begin_block:end_block]
  1196. curr_dur = t[begin_block] - bd[begin_block]
  1197. for block_counter in blocks:
  1198. block = self.get_block(block_counter)
  1199. if block.rf is not None: # RF
  1200. rf = block.rf
  1201. t = rf.delay + calc_rf_center(rf)[0]
  1202. if not hasattr(rf, "use") or block.rf.use in [
  1203. "excitation",
  1204. "undefined",
  1205. ]:
  1206. t_excitation.append(curr_dur + t)
  1207. fp_excitation.append([block.rf.freq_offset, block.rf.phase_offset])
  1208. elif block.rf.use == "refocusing":
  1209. t_refocusing.append(curr_dur + t)
  1210. fp_refocusing.append([block.rf.freq_offset, block.rf.phase_offset])
  1211. curr_dur += self.block_durations[block_counter]
  1212. if len(t_excitation) != 0:
  1213. fp_excitation = np.array(fp_excitation).T
  1214. else:
  1215. fp_excitation = np.empty((2, 0))
  1216. if len(t_refocusing) != 0:
  1217. fp_refocusing = np.array(fp_refocusing).T
  1218. else:
  1219. fp_refocusing = np.empty((2, 0))
  1220. return t_excitation, fp_excitation, t_refocusing, fp_refocusing
  1221. def set_block(self, block_index: int, *args: SimpleNamespace) -> None:
  1222. """
  1223. Replace block at index with new block provided as block structure, add sequence block, or create a new block
  1224. from events and store at position specified by index. The block or events are provided in uncompressed form and
  1225. will be stored in the compressed, non-redundant internal libraries.
  1226. See also:
  1227. - `pypulseq.Sequence.sequence.Sequence.get_block()`
  1228. - `pypulseq.Sequence.sequence.Sequence.add_block()`
  1229. Parameters
  1230. ----------
  1231. block_index : int
  1232. Index at which block is replaced.
  1233. args : SimpleNamespace
  1234. Block or events to be replaced/added or created at `block_index`.
  1235. """
  1236. block.set_block(self, block_index, *args)
  1237. if block_index >= self.next_free_block_ID:
  1238. self.next_free_block_ID = block_index + 1
  1239. def set_definition(
  1240. self, key: str, value: Union[float, int, list, np.ndarray, str, tuple]
  1241. ) -> None:
  1242. """
  1243. Modify a custom definition of the sequence. Set the user definition 'key' to value 'value'. If the definition
  1244. does not exist it will be created.
  1245. See also `pypulseq.Sequence.sequence.Sequence.get_definition()`.
  1246. Parameters
  1247. ----------
  1248. key : str
  1249. Definition key.
  1250. value : int, list, np.ndarray, str or tuple
  1251. Definition value.
  1252. """
  1253. if key == "FOV":
  1254. if np.max(value) > 1:
  1255. text = "Definition FOV uses values exceeding 1 m. "
  1256. text += "New Pulseq interpreters expect values in units of meters."
  1257. warn(text)
  1258. self.definitions[key] = value
  1259. def set_extension_string_ID(self, extension_str: str, extension_id: int) -> None:
  1260. """
  1261. Set numeric ID for the given string extension ID.
  1262. Parameters
  1263. ----------
  1264. extension_str : str
  1265. Given string extension ID.
  1266. extension_id : int
  1267. Given numeric extension ID.
  1268. Raises
  1269. ------
  1270. ValueError
  1271. If given numeric or string extension ID is not unique.
  1272. """
  1273. if (
  1274. extension_str in self.extension_string_idx
  1275. or extension_id in self.extension_numeric_idx
  1276. ):
  1277. raise ValueError("Numeric or string ID is not unique")
  1278. self.extension_numeric_idx.append(extension_id)
  1279. self.extension_string_idx.append(extension_str)
  1280. assert len(self.extension_numeric_idx) == len(self.extension_string_idx)
  1281. def test_report(self) -> str:
  1282. """
  1283. Analyze the sequence and return a text report.
  1284. """
  1285. return ext_test_report(self)
  1286. def waveforms(
  1287. self, append_RF: bool = False, time_range: List[float] = None
  1288. ) -> Tuple[np.ndarray]:
  1289. """
  1290. Decompress the entire gradient waveform. Returns gradient waveforms as a tuple of `np.ndarray` of
  1291. `gradient_axes` (typically 3) dimensions. Each `np.ndarray` contains timepoints and the corresponding
  1292. gradient amplitude values.
  1293. Parameters
  1294. ----------
  1295. append_RF : bool, default=False
  1296. Boolean flag to indicate if RF wave shapes are to be appended after the gradients.
  1297. Returns
  1298. -------
  1299. wave_data : np.ndarray
  1300. """
  1301. grad_channels = ["gx", "gy", "gz"]
  1302. # Collect shape pieces
  1303. if append_RF:
  1304. shape_channels = len(grad_channels) + 1 # Last 'channel' is RF
  1305. else:
  1306. shape_channels = len(grad_channels)
  1307. shape_pieces = [[] for _ in range(shape_channels)]
  1308. out_len = np.zeros(shape_channels) # Last 'channel' is RF
  1309. curr_dur = 0
  1310. if time_range == None:
  1311. blocks = self.block_events
  1312. else:
  1313. if len(time_range) != 2:
  1314. raise ValueError('Time range must be list of two elements')
  1315. if time_range[0] > time_range[1]:
  1316. raise ValueError('End time of time_range must be after begin time')
  1317. # Calculate end times of each block
  1318. bd = np.array(list(self.block_durations.values()))
  1319. t = np.cumsum(bd)
  1320. # Search block end times for start of time range
  1321. begin_block = np.searchsorted(t, time_range[0])
  1322. # Search block begin times for end of time range
  1323. end_block = np.searchsorted(t - bd, time_range[1], side='right')
  1324. blocks = list(self.block_durations.keys())[begin_block:end_block]
  1325. curr_dur = t[begin_block] - bd[begin_block]
  1326. for block_counter in blocks:
  1327. block = self.get_block(block_counter)
  1328. for j in range(len(grad_channels)):
  1329. grad = getattr(block, grad_channels[j])
  1330. if grad is not None: # Gradients
  1331. if grad.type == "grad":
  1332. # Check if we have an extended trapezoid or an arbitrary gradient on a regular raster
  1333. tt_rast = grad.tt / self.grad_raster_time + 0.5
  1334. if np.all(
  1335. np.abs(tt_rast - np.arange(1, len(tt_rast) + 1)) < eps
  1336. ): # Arbitrary gradient
  1337. """
  1338. Arbitrary gradient: restore & recompress shape - if we had a trapezoid converted to shape we
  1339. have to find the "corners" and we can eliminate internal samples on the straight segments
  1340. but first we have to restore samples on the edges of the gradient raster intervals for that
  1341. we need the first sample.
  1342. """
  1343. # TODO: Implement restoreAdditionalShapeSamples
  1344. # https://github.com/pulseq/pulseq/blob/master/matlab/%2Bmr/restoreAdditionalShapeSamples.m
  1345. out_len[j] += len(grad.tt) + 2
  1346. shape_pieces[j].append(np.array(
  1347. [
  1348. curr_dur + grad.delay + np.concatenate(
  1349. ([0], grad.tt, [grad.tt[-1] + self.grad_raster_time / 2])),
  1350. np.concatenate(([grad.first], grad.waveform, [grad.last]))
  1351. ]
  1352. ))
  1353. else: # Extended trapezoid
  1354. out_len[j] += len(grad.tt)
  1355. shape_pieces[j].append(np.array(
  1356. [
  1357. curr_dur + grad.delay + grad.tt,
  1358. grad.waveform,
  1359. ]
  1360. ))
  1361. else:
  1362. if abs(grad.flat_time) > eps:
  1363. out_len[j] += 4
  1364. _temp = np.vstack(
  1365. (
  1366. cumsum(
  1367. curr_dur + grad.delay,
  1368. grad.rise_time,
  1369. grad.flat_time,
  1370. grad.fall_time,
  1371. ),
  1372. grad.amplitude * np.array([0, 1, 1, 0]),
  1373. )
  1374. )
  1375. shape_pieces[j].append(_temp)
  1376. else:
  1377. if abs(grad.rise_time) > eps and abs(grad.fall_time) > eps:
  1378. out_len[j] += 3
  1379. _temp = np.vstack(
  1380. (
  1381. cumsum(curr_dur + grad.delay, grad.rise_time, grad.fall_time),
  1382. grad.amplitude * np.array([0, 1, 0]),
  1383. )
  1384. )
  1385. shape_pieces[j].append(_temp)
  1386. else:
  1387. if abs(grad.amplitude) > eps:
  1388. print(
  1389. 'Warning: "empty" gradient with non-zero magnitude detected in block {}'.format(
  1390. block_counter))
  1391. if block.rf is not None: # RF
  1392. rf = block.rf
  1393. if append_RF:
  1394. rf_piece = np.array(
  1395. [
  1396. curr_dur + rf.delay + rf.t,
  1397. rf.signal
  1398. * np.exp(
  1399. 1j
  1400. * (rf.phase_offset + 2 * np.pi * rf.freq_offset * rf.t)
  1401. ),
  1402. ]
  1403. )
  1404. out_len[-1] += len(rf.t)
  1405. if abs(rf.signal[0]) > 0:
  1406. pre = np.array([[rf_piece[0, 0] - 0.1 * self.system.rf_raster_time], [0]])
  1407. rf_piece = np.hstack((pre, rf_piece))
  1408. out_len[-1] += pre.shape[1]
  1409. if abs(rf.signal[-1]) > 0:
  1410. post = np.array([[rf_piece[0, -1] + 0.1 * self.system.rf_raster_time], [0]])
  1411. rf_piece = np.hstack((rf_piece, post))
  1412. out_len[-1] += post.shape[1]
  1413. shape_pieces[-1].append(rf_piece)
  1414. curr_dur += self.block_durations[block_counter]
  1415. # Collect wave data
  1416. wave_data = []
  1417. for j in range(shape_channels):
  1418. if shape_pieces[j] == []:
  1419. wave_data.append(np.zeros((2, 0)))
  1420. continue
  1421. # If the first element of the next shape has the same time as
  1422. # the last element of the previous shape, drop the first
  1423. # element of the next shape.
  1424. shape_pieces[j] = ([shape_pieces[j][0]] +
  1425. [cur if prev[0, -1] + eps < cur[0, 0] else cur[:, 1:]
  1426. for prev, cur in zip(shape_pieces[j][:-1], shape_pieces[j][1:])])
  1427. wave_data.append(np.concatenate(shape_pieces[j], axis=1))
  1428. rftdiff = np.diff(wave_data[j][0])
  1429. if np.any(rftdiff < eps):
  1430. raise Warning(
  1431. "Time vector elements are not monotonically increasing."
  1432. )
  1433. return wave_data
  1434. def waveforms_and_times(
  1435. self, append_RF: bool = False, time_range: List[float] = None
  1436. ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
  1437. """
  1438. Decompress the entire gradient waveform. Returns gradient waveforms as a tuple of `np.ndarray` of
  1439. `gradient_axes` (typically 3) dimensions. Each `np.ndarray` contains timepoints and the corresponding
  1440. gradient amplitude values. Additional return values are time points of excitations, refocusings and ADC
  1441. sampling points.
  1442. Parameters
  1443. ----------
  1444. append_RF : bool, default=False
  1445. Boolean flag to indicate if RF wave shapes are to be appended after the gradients.
  1446. Returns
  1447. -------
  1448. wave_data : np.ndarray
  1449. tfp_excitation : np.ndarray
  1450. Contains time moments, frequency and phase offsets of the excitation RF pulses (similar for `
  1451. tfp_refocusing`).
  1452. tfp_refocusing : np.ndarray
  1453. t_adc: np.ndarray
  1454. Contains times of all ADC sample points.
  1455. fp_adc : np.ndarray
  1456. Contains frequency and phase offsets of each ADC object (not samples).
  1457. """
  1458. wave_data = self.waveforms(append_RF=append_RF, time_range=time_range)
  1459. t_excitation, fp_excitation, t_refocusing, fp_refocusing = self.rf_times(time_range=time_range)
  1460. t_adc, fp_adc = self.adc_times(time_range=time_range)
  1461. # Join times, frequency and phases of RF pulses for compatibility with previous implementation
  1462. tfp_excitation = np.concatenate((np.array(t_excitation)[None], fp_excitation), axis=0)
  1463. tfp_refocusing = np.concatenate((np.array(t_refocusing)[None], fp_refocusing), axis=0)
  1464. return wave_data, tfp_excitation, tfp_refocusing, t_adc, fp_adc
  1465. def waveforms_export(self, time_range=(0, np.inf)) -> dict:
  1466. """
  1467. Plot `Sequence`.
  1468. Parameters
  1469. ----------
  1470. time_range : iterable, default=(0, np.inf)
  1471. Time range (x-axis limits) for all waveforms. Default is 0 to infinity (entire sequence).
  1472. Returns
  1473. -------
  1474. all_waveforms: dict
  1475. Dictionary containing the following sequence waveforms and time array(s):
  1476. - `t_adc` - ADC timing array [seconds]
  1477. - `t_rf` - RF timing array [seconds]
  1478. - `t_rf_centers`: `rf_t_centers`,
  1479. - `t_gx`: x gradient timing array,
  1480. - `t_gy`: y gradient timing array,
  1481. - `t_gz`: z gradient timing array,
  1482. - `adc` - ADC complex signal (amplitude=1, phase=adc phase) [a.u.]
  1483. - `rf` - RF complex signal
  1484. - `rf_centers`: RF centers array,
  1485. - `gx` - x gradient
  1486. - `gy` - y gradient
  1487. - `gz` - z gradient
  1488. - `grad_unit`: [kHz/m],
  1489. - `rf_unit`: [Hz],
  1490. - `time_unit`: [seconds],
  1491. """
  1492. # Check time range validity
  1493. if (
  1494. not all([isinstance(x, (int, float)) for x in time_range])
  1495. or len(time_range) != 2
  1496. ):
  1497. raise ValueError("Invalid time range")
  1498. t0 = 0
  1499. adc_t_all = np.array([])
  1500. adc_signal_all = np.array([], dtype=complex)
  1501. rf_t_all = np.array([])
  1502. rf_signal_all = np.array([], dtype=complex)
  1503. rf_t_centers = np.array([])
  1504. rf_signal_centers = np.array([], dtype=complex)
  1505. gx_t_all = np.array([])
  1506. gy_t_all = np.array([])
  1507. gz_t_all = np.array([])
  1508. gx_all = np.array([])
  1509. gy_all = np.array([])
  1510. gz_all = np.array([])
  1511. for block_counter in self.block_events: # For each block
  1512. block = self.get_block(block_counter) # Retrieve it
  1513. is_valid = (
  1514. time_range[0] <= t0 <= time_range[1]
  1515. ) # Check if "current time" is within requested range.
  1516. if is_valid:
  1517. # Case 1: ADC
  1518. if block.adc != None:
  1519. adc = block.adc # Get adc info
  1520. # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this
  1521. # is the present convention - the samples are shifted by 0.5 dwell
  1522. t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell
  1523. adc_t = t0 + t
  1524. adc_signal = np.exp(1j * adc.phase_offset) * np.exp(
  1525. 1j * 2 * np.pi * t * adc.freq_offset
  1526. )
  1527. adc_t_all = np.concatenate((adc_t_all, adc_t))
  1528. adc_signal_all = np.concatenate((adc_signal_all, adc_signal))
  1529. if block.rf != None:
  1530. rf = block.rf
  1531. tc, ic = calc_rf_center(rf)
  1532. t = rf.t + rf.delay
  1533. tc = tc + rf.delay
  1534. # Debug - visualize
  1535. # sp12.plot(t_factor * (t0 + t), np.abs(rf.signal))
  1536. # sp13.plot(t_factor * (t0 + t), np.angle(rf.signal * np.exp(1j * rf.phase_offset)
  1537. # * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)),
  1538. # t_factor * (t0 + tc), np.angle(rf.signal[ic] * np.exp(1j * rf.phase_offset)
  1539. # * np.exp(1j * 2 * math.pi * rf.t[ic] * rf.freq_offset)),
  1540. # 'xb')
  1541. rf_t = t0 + t
  1542. rf = (
  1543. rf.signal
  1544. * np.exp(1j * rf.phase_offset)
  1545. * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)
  1546. )
  1547. rf_t_all = np.concatenate((rf_t_all, rf_t))
  1548. rf_signal_all = np.concatenate((rf_signal_all, rf))
  1549. rf_t_centers = np.concatenate((rf_t_centers, [rf_t[ic]]))
  1550. rf_signal_centers = np.concatenate((rf_signal_centers, [rf[ic]]))
  1551. grad_channels = ["gx", "gy", "gz"]
  1552. for x in range(
  1553. len(grad_channels)
  1554. ): # Check each gradient channel: x, y, and z
  1555. if getattr(block, grad_channels[x]) != None:
  1556. # If this channel is on in current block
  1557. grad = getattr(block, grad_channels[x])
  1558. if grad.type == "grad": # Arbitrary gradient option
  1559. # In place unpacking of grad.t with the starred expression
  1560. g_t = (
  1561. t0
  1562. + grad.delay
  1563. + [
  1564. 0,
  1565. *(grad.tt + (grad.tt[1] - grad.tt[0]) / 2),
  1566. grad.tt[-1] + grad.tt[1] - grad.tt[0],
  1567. ]
  1568. )
  1569. g = 1e-3 * np.array((grad.first, *grad.waveform, grad.last))
  1570. else: # Trapezoid gradient option
  1571. g_t = cumsum(
  1572. t0,
  1573. grad.delay,
  1574. grad.rise_time,
  1575. grad.flat_time,
  1576. grad.fall_time,
  1577. )
  1578. g = 1e-3 * grad.amplitude * np.array([0, 0, 1, 1, 0])
  1579. if grad.channel == "x":
  1580. gx_t_all = np.concatenate((gx_t_all, g_t))
  1581. gx_all = np.concatenate((gx_all, g))
  1582. elif grad.channel == "y":
  1583. gy_t_all = np.concatenate((gy_t_all, g_t))
  1584. gy_all = np.concatenate((gy_all, g))
  1585. elif grad.channel == "z":
  1586. gz_t_all = np.concatenate((gz_t_all, g_t))
  1587. gz_all = np.concatenate((gz_all, g))
  1588. t0 += self.block_durations[
  1589. block_counter
  1590. ] # "Current time" gets updated to end of block just examined
  1591. all_waveforms = {
  1592. "t_adc": adc_t_all,
  1593. "t_rf": rf_t_all,
  1594. "t_rf_centers": rf_t_centers,
  1595. "t_gx": gx_t_all,
  1596. "t_gy": gy_t_all,
  1597. "t_gz": gz_t_all,
  1598. "adc": adc_signal_all,
  1599. "rf": rf_signal_all,
  1600. "rf_centers": rf_signal_centers,
  1601. "gx": gx_all,
  1602. "gy": gy_all,
  1603. "gz": gz_all,
  1604. "grad_unit": "[kHz/m]",
  1605. "rf_unit": "[Hz]",
  1606. "time_unit": "[seconds]",
  1607. }
  1608. return all_waveforms
  1609. def write(self, name: str, create_signature: bool = True, remove_duplicates: bool = True) -> Union[str, None]:
  1610. """
  1611. Write the sequence data to the given filename using the open file format for MR sequences.
  1612. See also `pypulseq.Sequence.read_seq.read()`.
  1613. Parameters
  1614. ----------
  1615. name : str
  1616. Filename of `.seq` file to be written to disk.
  1617. create_signature : bool, default=True
  1618. Boolean flag to indicate if the file has to be signed.
  1619. remove_duplicates : bool, default=True
  1620. Remove duplicate events from the sequence before writing
  1621. Returns
  1622. -------
  1623. signature or None : If create_signature is True, it returns the written .seq file's signature as a string,
  1624. otherwise it returns None. Note that, if remove_duplicates is True, signature belongs to the
  1625. deduplicated sequences signature, and not the Sequence that is stored in the Sequence object.
  1626. """
  1627. signature = write_seq(self, name, create_signature)
  1628. if signature is not None:
  1629. self.signature_type = "md5"
  1630. self.signature_file = "text"
  1631. self.signature_value = signature
  1632. return signature
  1633. else:
  1634. return None