sequence.py 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703
  1. import itertools
  2. import math
  3. from collections import OrderedDict
  4. from copy import copy
  5. from itertools import chain
  6. from types import SimpleNamespace
  7. from typing import Tuple, List, Iterable
  8. from typing import Union
  9. from warnings import warn
  10. import matplotlib as mpl
  11. import numpy as np
  12. from matplotlib import pyplot as plt
  13. from seqgen.pypulseq import eps
  14. from seqgen.pypulseq.Sequence import block, parula
  15. from seqgen.pypulseq.Sequence.ext_test_report import ext_test_report
  16. from seqgen.pypulseq.Sequence.read_seq import read
  17. from seqgen.pypulseq.Sequence.write_seq import write as write_seq
  18. from seqgen.pypulseq.calc_rf_center import calc_rf_center
  19. from seqgen.pypulseq.check_timing import check_timing as ext_check_timing
  20. from seqgen.pypulseq.decompress_shape import decompress_shape
  21. from seqgen.pypulseq.event_lib import EventLibrary
  22. from seqgen.pypulseq.opts import Opts
  23. from seqgen.pypulseq.supported_labels_rf_use import get_supported_labels
  24. from seqgen.MRI_sequences.version import major, minor, revision
  25. class Sequence:
  26. """
  27. Generate sequences and read/write sequence files. This class defines properties and methods to define a complete MR
  28. sequence including RF pulses, gradients, ADC events, etc. The class provides an implementation of the open MR
  29. sequence format defined by the Pulseq project. See http://pulseq.github.io/.
  30. See also `demo_read.py`, `demo_write.py`.
  31. """
  32. version_major = major
  33. version_minor = minor
  34. version_revision = revision
  35. def __init__(self, system=Opts()):
  36. # =========
  37. # EVENT LIBRARIES
  38. # =========
  39. self.adc_library = EventLibrary() # Library of ADC events
  40. self.delay_library = EventLibrary() # Library of delay events
  41. # Library of extension events. Extension events form single-linked zero-terminated lists
  42. self.extensions_library = EventLibrary()
  43. self.grad_library = EventLibrary() # Library of gradient events
  44. self.label_inc_library = (
  45. EventLibrary()
  46. ) # Library of Label(inc) events (reference from the extensions library)
  47. self.label_set_library = (
  48. EventLibrary()
  49. ) # Library of Label(set) events (reference from the extensions library)
  50. self.rf_library = EventLibrary() # Library of RF events
  51. self.shape_library = EventLibrary() # Library of compressed shapes
  52. self.trigger_library = EventLibrary() # Library of trigger events
  53. # =========
  54. # OTHER
  55. # =========
  56. self.system = system
  57. self.block_events = OrderedDict() # Event table
  58. self.definitions = dict() # Optional sequence definitions
  59. self.rf_raster_time = (
  60. self.system.rf_raster_time
  61. ) # RF raster time (system dependent)
  62. self.grad_raster_time = (
  63. self.system.grad_raster_time
  64. ) # Gradient raster time (system dependent)
  65. self.adc_raster_time = (
  66. self.system.adc_raster_time
  67. ) # ADC raster time (system dependent)
  68. self.block_duration_raster = self.system.block_duration_raster
  69. self.set_definition("AdcRasterTime", self.adc_raster_time)
  70. self.set_definition("BlockDurationRaster", self.block_duration_raster)
  71. self.set_definition("GradientRasterTime", self.grad_raster_time)
  72. self.set_definition("RadiofrequencyRasterTime", self.rf_raster_time)
  73. self.signature_type = ""
  74. self.signature_file = ""
  75. self.signature_value = ""
  76. self.block_durations = [] # Cache of block durations
  77. self.extension_numeric_idx = [] # numeric IDs of the used extensions
  78. self.extension_string_idx = [] # string IDs of the used extensions
  79. def __str__(self) -> str:
  80. s = "Sequence:"
  81. s += "\nshape_library: " + str(self.shape_library)
  82. s += "\nrf_library: " + str(self.rf_library)
  83. s += "\ngrad_library: " + str(self.grad_library)
  84. s += "\nadc_library: " + str(self.adc_library)
  85. s += "\ndelay_library: " + str(self.delay_library)
  86. s += "\nextensions_library: " + str(
  87. self.extensions_library
  88. ) # inserted for trigger support by mveldmann
  89. s += "\nrf_raster_time: " + str(self.rf_raster_time)
  90. s += "\ngrad_raster_time: " + str(self.grad_raster_time)
  91. s += "\nblock_events: " + str(len(self.block_events))
  92. return s
  93. def add_block(self, *args: SimpleNamespace) -> None:
  94. """
  95. Add a new block/multiple events to the sequence. Adds a sequence block with provided as a block structure
  96. See also:
  97. - `pypulseq.Sequence.sequence.Sequence.set_block()`
  98. - `pypulseq.make_adc.make_adc()`
  99. - `pypulseq.make_trapezoid.make_trapezoid()`
  100. - `pypulseq.make_sinc_pulse.make_sinc_pulse()`
  101. Parameters
  102. ----------
  103. args : SimpleNamespace
  104. Block structure or events to be added as a block to `Sequence`.
  105. """
  106. block.set_block(self, len(self.block_events) + 1, *args)
  107. def calculate_kspace(
  108. self, trajectory_delay: int = 0
  109. ) -> Tuple[np.array, np.array, np.array, np.array, np.array]:
  110. """
  111. Calculates the k-space trajectory of the entire pulse sequence.
  112. Parameters
  113. ----------
  114. trajectory_delay : int, default=0
  115. Compensation factor in seconds (s) to align ADC and gradients in the reconstruction.
  116. Returns
  117. -------
  118. k_traj_adc : numpy.array
  119. K-space trajectory sampled at `t_adc` timepoints.
  120. k_traj : numpy.array
  121. K-space trajectory of the entire pulse sequence.
  122. t_excitation : numpy.array
  123. Excitation timepoints.
  124. t_refocusing : numpy.array
  125. Refocusing timepoints.
  126. t_adc : numpy.array
  127. Sampling timepoints.
  128. """
  129. if np.any(np.abs(trajectory_delay) > 100e-6):
  130. raise Warning(
  131. f"Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high"
  132. )
  133. # Initialise the counters and accumulator objects
  134. count_excitation = 0
  135. count_refocusing = 0
  136. count_adc_samples = 0
  137. # Loop through the blocks to prepare preallocations
  138. for block_counter in range(len(self.block_events)):
  139. block = self.get_block(block_counter + 1)
  140. if block.rf is not None:
  141. if (
  142. not hasattr(block.rf, "use")
  143. or block.rf.use == "excitation"
  144. or block.rf.use == "undefined"
  145. ):
  146. count_excitation += 1
  147. elif block.rf.use == "refocusing":
  148. count_refocusing += 1
  149. if block.adc is not None:
  150. count_adc_samples += int(block.adc.num_samples)
  151. t_excitation = np.zeros(count_excitation)
  152. t_refocusing = np.zeros(count_refocusing)
  153. k_time = np.zeros(count_adc_samples)
  154. current_duration = 0
  155. count_excitation = 0
  156. count_refocusing = 0
  157. kc_outer = 0
  158. traj_recon_delay = trajectory_delay
  159. # Go through the blocks and collect RF and ADC timing data
  160. for block_counter in range(len(self.block_events)):
  161. block = self.get_block(block_counter + 1)
  162. if block.rf is not None:
  163. rf = block.rf
  164. rf_center, _ = calc_rf_center(rf)
  165. t = rf.delay + rf_center
  166. if (
  167. not hasattr(block.rf, "use")
  168. or block.rf.use == "excitation"
  169. or block.rf.use == "undefined"
  170. ):
  171. t_excitation[count_excitation] = current_duration + t
  172. count_excitation += 1
  173. elif block.rf.use == "refocusing":
  174. t_refocusing[count_refocusing] = current_duration + t
  175. count_refocusing += 1
  176. if block.adc is not None:
  177. _k_time = np.arange(block.adc.num_samples) + 0.5
  178. _k_time = (
  179. _k_time * block.adc.dwell
  180. + block.adc.delay
  181. + current_duration
  182. + traj_recon_delay
  183. )
  184. k_time[kc_outer : kc_outer + block.adc.num_samples] = _k_time
  185. kc_outer += block.adc.num_samples
  186. current_duration += self.block_durations[block_counter]
  187. # Now calculate the actual k-space trajectory based on the gradient waveforms
  188. gw = self.gradient_waveforms()
  189. i_excitation = np.round(t_excitation / self.grad_raster_time)
  190. i_refocusing = np.round(t_refocusing / self.grad_raster_time)
  191. i_periods = np.sort(
  192. [1, *(i_excitation + 1), *(i_refocusing + 1), gw.shape[1] + 1]
  193. ).astype(np.int32)
  194. # i_periods -= 1 # Python is 0-indexed
  195. ii_next_excitation = np.min((len(i_excitation), 1))
  196. ii_next_refocusing = np.min((len(i_refocusing), 1))
  197. k_traj = np.zeros_like(gw)
  198. k = np.zeros((3, 1))
  199. for i in range(len(i_periods) - 1):
  200. i_period_end = i_periods[i + 1] - 1
  201. k_period = np.concatenate(
  202. (k, gw[:, i_periods[i] - 1 : i_period_end] * self.grad_raster_time),
  203. axis=1,
  204. )
  205. k_period = np.cumsum(k_period, axis=1)
  206. k_traj[:, i_periods[i] - 1 : i_period_end] = k_period[:, 1:]
  207. k = k_period[:, -1]
  208. if (
  209. ii_next_excitation > 0
  210. and i_excitation[ii_next_excitation - 1] == i_period_end
  211. ):
  212. k[:] = 0
  213. k_traj[:, i_period_end - 1] = np.nan
  214. ii_next_excitation = min(len(i_excitation), ii_next_excitation + 1)
  215. if (
  216. ii_next_refocusing > 0
  217. and i_refocusing[ii_next_refocusing - 1] == i_period_end
  218. ):
  219. k = -k
  220. ii_next_refocusing = min(len(i_refocusing), ii_next_refocusing + 1)
  221. k = k.reshape((-1, 1)) # To be compatible with np.concatenate
  222. k_traj_adc = []
  223. for _k_traj_row in k_traj:
  224. result = np.interp(
  225. xp=np.array(range(1, k_traj.shape[1] + 1)) * self.grad_raster_time,
  226. fp=_k_traj_row,
  227. x=k_time,
  228. )
  229. k_traj_adc.append(result)
  230. k_traj_adc = np.stack(k_traj_adc)
  231. t_adc = k_time
  232. return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc
  233. def calculate_kspacePP(
  234. self,
  235. trajectory_delay: Union[int, float, np.ndarray] = 0,
  236. gradient_offset: int = 0,
  237. ) -> Tuple[np.array, np.array, np.array, np.array, np.array]:
  238. """
  239. Calculates the k-space trajectory of the entire pulse sequence.
  240. Parameters
  241. ----------
  242. trajectory_delay : int, default=0
  243. Compensation factor in seconds (s) to align ADC and gradients in the reconstruction.
  244. Returns
  245. -------
  246. k_traj_adc : numpy.array
  247. K-space trajectory sampled at `t_adc` timepoints.
  248. k_traj : numpy.array
  249. K-space trajectory of the entire pulse sequence.
  250. t_excitation : numpy.array
  251. Excitation timepoints.
  252. t_refocusing : numpy.array
  253. Refocusing timepoints.
  254. t_adc : numpy.array
  255. Sampling timepoints.
  256. """
  257. if np.any(np.abs(trajectory_delay) > 100e-6):
  258. raise Warning(
  259. f"Trajectory delay of {trajectory_delay * 1e6} us is suspiciously high"
  260. )
  261. total_duration = np.sum(self.block_durations)
  262. gw_data, tfp_excitation, tfp_refocusing, t_adc, _ = self.waveforms_and_times()
  263. ng = len(gw_data)
  264. # Gradient delay handling
  265. if isinstance(trajectory_delay, (int, float)):
  266. gradient_delays = [trajectory_delay] * ng
  267. else:
  268. assert (
  269. len(trajectory_delay) == ng
  270. ) # Need to have same number of gradient channels
  271. gradient_delays = [trajectory_delay] * ng
  272. # Gradient offset handling
  273. if isinstance(gradient_offset, (int, float)):
  274. gradient_offset = [gradient_offset] * ng
  275. else:
  276. assert (
  277. len(gradient_offset) == ng
  278. ) # Need to have same number of gradient channels
  279. # Convert data to piecewise polynomials
  280. gw_pp = []
  281. gw_pp_MATLAB = []
  282. for j in range(ng):
  283. wave_cnt = gw_data[j].shape[1]
  284. if wave_cnt == 0:
  285. if np.abs(gradient_offset[j]) <= eps:
  286. continue
  287. else:
  288. gw = np.array(([0, total_duration], [0, 0]))
  289. else:
  290. gw = gw_data[j]
  291. # Now gw contains the waveform from the current axis
  292. if np.abs(gradient_delays[j]) > eps:
  293. gw[1] = gw[1] - gradient_delays[j] # Anisotropic gradient delay support
  294. if not np.all(np.isfinite(gw)):
  295. raise Warning("Not all elements of the generated waveform are finite.")
  296. teps = 1e-12
  297. if gw[0, 0] > 0 and gw[1, -1] < total_duration:
  298. # teps terms to avoid integration errors over extended periods of time
  299. _temp1 = np.array(([-teps, gw[0, 0] - teps], [0, 0]))
  300. _temp2 = np.array(([gw[0, -1] + teps, total_duration + teps], [0, 0]))
  301. gw = np.hstack((_temp1, gw, _temp2))
  302. elif gw[0, 0] > 0:
  303. _temp = np.array(([-teps, gw[0, 0] - teps], [0, 0]))
  304. gw = np.hstack((_temp, gw))
  305. elif gw[0, -1] < total_duration:
  306. _temp = np.array(([gw[0, -1] + teps, total_duration + teps], [0, 0]))
  307. gw = np.hstack((gw, _temp))
  308. if np.abs(gradient_offset[j]) > eps:
  309. gw[1:] += gradient_offset[j]
  310. gw[1][gw[1] == -0.0] = 0.0
  311. # Specify window to be same as domain prevent numpy from remapping to [-1, 1]
  312. polyfit = [
  313. np.polynomial.Polynomial.fit(
  314. gw[0, i : (i + 2)],
  315. gw[1, i : (i + 2)],
  316. deg=1,
  317. window=gw[0, i : (i + 2)],
  318. )
  319. for i in range(len(gw[0]) - 1)
  320. ]
  321. polyfit = np.stack(polyfit)
  322. gw_pp.append(polyfit)
  323. ###
  324. """
  325. Fix coefs for consistency with MATLAB:
  326. 1. MATLAB returns coefficients in descending order whereas Numpy returns coefficients in ascending order.
  327. 2. MATLAB returns local coefficients that will NOT match Numpy's outputs. Refer to the equation under the
  328. "output arguments" section of `mkpp` MATLAB docs to convert and match coefficient outputs.
  329. 3. Finally, MATLAB seems to not store any -1 < x < 1 coefficients, so we zero them out.
  330. """
  331. polyfit_MATLAB = []
  332. for i in range(len(polyfit)):
  333. polyfit_MATLAB_i = copy(polyfit[i])
  334. lower = polyfit_MATLAB_i.domain[0]
  335. co = polyfit_MATLAB_i.coef
  336. co = co[::-1] # Reverse
  337. a = co[0]
  338. b = co[1] + (a * lower)
  339. if -1 < a < 1: # to investigate
  340. a = 0
  341. if -1 < b < 1:
  342. b = 0
  343. # co = [b, a] # Un-reverse for Numpy
  344. co = [a, b]
  345. polyfit_MATLAB_i.coef = co
  346. polyfit_MATLAB.append(polyfit_MATLAB_i)
  347. gw_pp_MATLAB.append(polyfit_MATLAB)
  348. ###
  349. # Calculate slice positions. For now we entirely rely on the excitation -- ignoring complicated interleaved
  350. # refocused sequences
  351. if len(tfp_excitation) > 0:
  352. # Position in x, y, z
  353. slice_pos = np.zeros((len(gw_data), tfp_excitation.shape[1]))
  354. for j in range(len(gw_data)):
  355. if gw_pp[j] is None:
  356. slice_pos[j] = np.empty_like((1, slice_pos.shape[1]))
  357. else:
  358. slice_pos[j] = np.divide(
  359. tfp_excitation[1], self.ppval_numpy(gw_pp[j], tfp_excitation[0])
  360. )
  361. slice_pos[~np.isfinite(slice_pos)] = 0 # Reset undefined to 0
  362. t_slice_pos = tfp_excitation[0]
  363. else:
  364. slice_pos = []
  365. t_slice_pos = []
  366. # FNINT
  367. def fnint(arr_poly):
  368. pieces = len(arr_poly)
  369. breaks = np.stack([pp.domain for pp in arr_poly])
  370. breaks = np.append(breaks[:, 0], breaks[-1, 1])
  371. coefs = np.stack([pp.coef for pp in arr_poly])
  372. order = len(arr_poly[1].coef)
  373. dim = 1
  374. coefs = coefs / np.tile(range(order, 0, -1), (dim * pieces, 1))
  375. xs = np.diff(breaks[:-1])
  376. index = np.arange(pieces - 1)
  377. vv = xs * coefs[index, 0]
  378. for i in range(1, order):
  379. vv = xs * (vv + coefs[index, i])
  380. last = np.cumsum(np.insert(vv, 0, 0)).reshape((-1, 1))
  381. coefs = np.hstack((coefs[:, :order], last))
  382. arr_poly_integ = []
  383. for i in range(pieces):
  384. arr_poly_integ.append(
  385. np.polynomial.Polynomial(
  386. coefs[i],
  387. domain=[breaks[i], breaks[i + 1]],
  388. window=[breaks[i], breaks[i + 1]],
  389. )
  390. )
  391. return arr_poly_integ, coefs, breaks
  392. # =========
  393. # Integrate waveforms as PPs to produce gradient moments
  394. gm_pp = []
  395. tc = []
  396. for i in range(ng):
  397. if gw_pp[i] is None:
  398. continue
  399. res_fnint, res_coefs, res_breaks = fnint(gw_pp_MATLAB[i])
  400. gm_pp.append(res_fnint)
  401. tc.append(res_breaks)
  402. # "Sample" ramps for display purposes otherwise piecewise-linear display (plot) fails
  403. ii = np.nonzero(np.abs(res_coefs[:, 0]) > eps)[0]
  404. if len(ii) > 0:
  405. tca = []
  406. for j in range(len(ii)):
  407. res = (
  408. np.arange(
  409. np.floor(float(res_breaks[ii[j]] / self.grad_raster_time)),
  410. np.ceil(
  411. (res_breaks[ii[j] + 1] / self.grad_raster_time) + 1
  412. ),
  413. )
  414. * self.grad_raster_time
  415. )
  416. tca.extend(res)
  417. tc.append(tca)
  418. tc = np.array(list(chain(*tc)))
  419. if len(tfp_excitation) == 0:
  420. t_excitation = np.array([])
  421. else:
  422. t_excitation = tfp_excitation[0]
  423. if len(tfp_refocusing) == 0:
  424. t_refocusing = np.array([])
  425. else:
  426. t_refocusing = tfp_refocusing[0]
  427. t_acc = 1e-10 # Temporal accuracy
  428. t_acc_inv = 1 / t_acc
  429. # tc = self.__flatten_jagged_arr(tc)
  430. t_ktraj = t_acc * np.unique(
  431. np.round(
  432. t_acc_inv
  433. * np.array(
  434. [
  435. *tc,
  436. 0,
  437. *np.array(t_excitation) * 2 - self.rf_raster_time,
  438. *np.array(t_excitation) - self.rf_raster_time,
  439. *t_excitation,
  440. *np.array(t_refocusing) - self.rf_raster_time,
  441. *t_refocusing,
  442. *t_adc,
  443. total_duration,
  444. ]
  445. )
  446. )
  447. )
  448. i_excitation = np.isin(t_ktraj, t_acc * np.round(t_acc_inv * t_excitation))
  449. i_excitation = np.nonzero(i_excitation)[0] # Convert boolean array into indices
  450. i_refocusing = np.isin(t_ktraj, t_acc * np.round(t_acc_inv * t_refocusing))
  451. i_refocusing = np.nonzero(i_refocusing)[0] # Convert boolean array into indices
  452. i_adc = np.isin(t_ktraj, t_acc * np.round(t_acc_inv * t_adc))
  453. i_adc = np.nonzero(i_adc)[0] # Convert boolean array into indices
  454. i_periods = np.unique([1, *i_excitation, *i_refocusing, len(t_ktraj) - 1])
  455. if len(i_excitation) > 0:
  456. ii_next_excitation = 1
  457. else:
  458. ii_next_excitation = 0
  459. if len(i_refocusing) > 0:
  460. ii_next_refocusing = 1
  461. else:
  462. ii_next_refocusing = 0
  463. k_traj = np.zeros((3, len(t_ktraj)))
  464. for i in range(ng):
  465. if gw_pp_MATLAB[i] is None:
  466. continue
  467. it = np.logical_and(
  468. t_ktraj >= t_acc * np.round(t_acc_inv * res_breaks[0]),
  469. t_ktraj <= t_acc * np.round(t_acc_inv * res_breaks[-1]),
  470. )
  471. k_traj[i, it] = self.ppval_MATLAB(gm_pp[i], t_ktraj[it])
  472. if t_ktraj[it[-1]] < t_ktraj[-1]:
  473. k_traj[i, it[-1] + 1 :] = k_traj[i, it[-1]]
  474. # Convert gradient moments to kspace
  475. dk = -k_traj[:, 0]
  476. for i in range(len(i_periods) - 1):
  477. i_period = i_periods[i]
  478. i_period_end = i_periods[i + 1]
  479. if ii_next_excitation > 0 and i_excitation[ii_next_excitation] == i_period:
  480. if np.abs(t_ktraj[i_period] - t_excitation[ii_next_excitation]) > t_acc:
  481. raise Warning(
  482. f"np.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)}"
  483. )
  484. dk = -k_traj[:, i_period]
  485. if i_period > 1:
  486. # Use nans to mark the excitation points since they interrupt the plots
  487. k_traj[:, i_period - 1] = np.NaN
  488. # -1 on len(i_excitation) for 0-based indexing
  489. ii_next_excitation = np.minimum(
  490. len(i_excitation) - 1, ii_next_excitation + 1
  491. )
  492. elif (
  493. ii_next_refocusing > 0 and i_refocusing[ii_next_refocusing] == i_period
  494. ):
  495. dk = -k_traj[:, i_period]
  496. dk = -2 * k_traj[:, i_period] - dk
  497. # -1 on len(i_excitation) for 0-based indexing
  498. ii_next_refocusing = np.minimum(
  499. len(i_refocusing) - 1, ii_next_refocusing + 1
  500. )
  501. k_traj[:, i_period:i_period_end] = (
  502. k_traj[:, i_period:i_period_end] + dk[:, None]
  503. )
  504. k_traj[:, i_period_end] = k_traj[:, i_period_end] + dk
  505. k_traj_adc = k_traj[:, i_adc]
  506. return k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc
  507. def check_timing(self) -> Tuple[bool, List[str]]:
  508. """
  509. Checks timing of all blocks and objects in the sequence optionally returns the detailed error log. This
  510. function also modifies the sequence object by adding the field "TotalDuration" to sequence definitions.
  511. Returns
  512. -------
  513. is_ok : bool
  514. Boolean flag indicating timing errors.
  515. error_report : str
  516. Error report in case of timing errors.
  517. """
  518. error_report = []
  519. is_ok = True
  520. num_blocks = len(self.block_events)
  521. total_duration = 0
  522. for block_counter in range(num_blocks):
  523. block = self.get_block(block_counter + 1)
  524. events = [e for e in vars(block).values() if e is not None]
  525. res, rep, duration = ext_check_timing(self.system, *events)
  526. is_ok = is_ok and res
  527. # Check the stored total block duration
  528. if np.abs(duration - self.block_durations[block_counter]) > eps:
  529. rep += "Inconsistency between the stored block duration and the duration of the block content"
  530. is_ok = False
  531. duration = self.block_durations[block_counter]
  532. # Check RF dead times
  533. if block.rf is not None:
  534. if block.rf.delay - block.rf.dead_time < -eps:
  535. rep += (
  536. f"Delay of {block.rf.delay * 1e6} us is smaller than the RF dead time "
  537. f"{block.rf.dead_time * 1e6} us"
  538. )
  539. is_ok = False
  540. if (
  541. block.rf.delay + block.rf.t[-1] + block.rf.ringdown_time - duration
  542. > eps
  543. ):
  544. rep += (
  545. f"Time between the end of the RF pulse at {block.rf.delay + block.rf.t[-1]} and the end "
  546. f"of the block at {duration * 1e6} us is shorter than rf_ringdown_time"
  547. )
  548. is_ok = False
  549. # Check ADC dead times
  550. if block.adc is not None:
  551. if block.adc.delay - self.system.adc_dead_time < -eps:
  552. rep += "adc.delay < system.adc_dead_time"
  553. is_ok = False
  554. if (
  555. block.adc.delay
  556. + block.adc.num_samples * block.adc.dwell
  557. + self.system.adc_dead_time
  558. - duration
  559. > eps
  560. ):
  561. rep += "adc: system.adc_dead_time (post-ADC) violation"
  562. is_ok = False
  563. # Update report
  564. if len(rep) != 0:
  565. error_report.append(f"Event: {block_counter} - {rep}\n")
  566. total_duration += duration
  567. # Check if all the gradients in the last block are ramped down properly
  568. if len(events) != 0 and all([isinstance(e, SimpleNamespace) for e in events]):
  569. for e in range(len(events)):
  570. if not isinstance(events[e], list) and events[e].type == "grad":
  571. if events[e].last != 0:
  572. error_report.append(
  573. f"Event {num_blocks - 1} gradients do not ramp to 0 at the end of the sequence"
  574. )
  575. self.set_definition("TotalDuration", total_duration)
  576. return is_ok, error_report
  577. def duration(self) -> Tuple[int, int, np.ndarray]:
  578. """
  579. Returns the total duration of this sequence, and the total count of blocks and events.
  580. Returns
  581. -------
  582. duration : int
  583. Duration of this sequence in seconds (s).
  584. num_blocks : int
  585. Number of blocks in this sequence.
  586. event_count : np.ndarray
  587. Number of events in this sequence.
  588. """
  589. num_blocks = len(self.block_events)
  590. event_count = np.zeros(len(self.block_events[1]))
  591. duration = 0
  592. for block_counter in range(num_blocks):
  593. event_count += self.block_events[block_counter + 1] > 0
  594. duration += self.block_durations[block_counter]
  595. return duration, num_blocks, event_count
  596. def __flatten_jagged_arr(self, arr: np.array) -> np.array:
  597. # Sanity check: we don't need to do anything if we have a flat array
  598. def __flat_check(arr: np.array) -> bool:
  599. return all([not isinstance(x, Iterable) for x in arr])
  600. if __flat_check(arr):
  601. return arr
  602. # Flatten the array simply -- 1 level deep
  603. arr_flattened = list(itertools.chain(*arr))
  604. # Not flat yet?
  605. if __flat_check(arr_flattened):
  606. return arr_flattened
  607. # Flatten the array -- 2 levels deep
  608. any_ragged = [isinstance(x, Iterable) for x in arr_flattened]
  609. if np.any(any_ragged):
  610. idx_ragged = np.array(np.where(any_ragged)[0])
  611. for i in range(len(idx_ragged)):
  612. ii = idx_ragged[i]
  613. # If we are not at the end of the list, we need to update the indices of the remaining elements
  614. # Because once we expand and insert this list element, the indices of the remaining elements
  615. # will be shifted by len(this element)
  616. if i != len(idx_ragged) - 1:
  617. idx_ragged[i + 1 :] += len(arr_flattened[ii])
  618. arr_flattened = np.insert(arr_flattened, ii, arr_flattened[ii])
  619. return arr_flattened
  620. def flip_grad_axis(self, axis: str) -> None:
  621. """
  622. Invert all gradients along the corresponding axis/channel. The function acts on all gradient objects already
  623. added to the sequence object.
  624. Parameters
  625. ----------
  626. axis : str
  627. Gradients to invert or scale. Must be one of 'x', 'y' or 'z'.
  628. """
  629. self.mod_grad_axis(axis, modifier=-1)
  630. def get_block(self, block_index: int) -> SimpleNamespace:
  631. """
  632. Return a block of the sequence specified by the index. The block is created from the sequence data with all
  633. events and shapes decompressed.
  634. See also:
  635. - `pypulseq.Sequence.sequence.Sequence.set_block()`.
  636. - `pypulseq.Sequence.sequence.Sequence.add_block()`.
  637. Parameters
  638. ----------
  639. block_index : int
  640. Index of block to be retrieved from `Sequence`.
  641. Returns
  642. -------
  643. SimpleNamespace
  644. Event identified by `block_index`.
  645. """
  646. return block.get_block(self, block_index)
  647. def get_definition(self, key: str) -> str:
  648. """
  649. Return value of the definition specified by the key. These definitions can be added manually or read from the
  650. header of a sequence file defined in the sequence header. An empty array is returned if the key is not defined.
  651. See also `pypulseq.Sequence.sequence.Sequence.set_definition()`.
  652. Parameters
  653. ----------
  654. key : str
  655. Key of definition to retrieve.
  656. Returns
  657. -------
  658. str
  659. Definition identified by `key` if found, else returns ''.
  660. """
  661. if key in self.definitions:
  662. return self.definitions[key]
  663. else:
  664. return ""
  665. def get_extension_type_ID(self, extension_string: str) -> int:
  666. """
  667. Get numeric extension ID for `extension_string`. Will automatically create a new ID if unknown.
  668. Parameters
  669. ----------
  670. extension_string : str
  671. Given string extension ID.
  672. Returns
  673. -------
  674. extension_id : int
  675. Numeric ID for given string extension ID.
  676. """
  677. if extension_string not in self.extension_string_idx:
  678. if len(self.extension_numeric_idx) == 0:
  679. extension_id = 1
  680. else:
  681. extension_id = 1 + max(self.extension_numeric_idx)
  682. self.extension_numeric_idx.append(extension_id)
  683. self.extension_string_idx.append(extension_string)
  684. assert len(self.extension_numeric_idx) == len(self.extension_string_idx)
  685. else:
  686. num = self.extension_string_idx.index(extension_string)
  687. extension_id = self.extension_numeric_idx[num]
  688. return extension_id
  689. def get_extension_type_string(self, extension_id: int) -> str:
  690. """
  691. Get string extension ID for `extension_id`.
  692. Parameters
  693. ----------
  694. extension_id : int
  695. Given numeric extension ID.
  696. Returns
  697. -------
  698. extension_str : str
  699. String ID for the given numeric extension ID.
  700. Raises
  701. ------
  702. ValueError
  703. If given numeric extension ID is unknown.
  704. """
  705. if extension_id in self.extension_numeric_idx:
  706. num = self.extension_numeric_idx.index(extension_id)
  707. else:
  708. raise ValueError(
  709. f"Extension for the given ID - {extension_id} - is unknown."
  710. )
  711. extension_str = self.extension_string_idx[num]
  712. return extension_str
  713. def mod_grad_axis(self, axis: str, modifier: int) -> None:
  714. """
  715. Invert or scale all gradients along the corresponding axis/channel. The function acts on all gradient objects
  716. already added to the sequence object.
  717. Parameters
  718. ----------
  719. axis : str
  720. Gradients to invert or scale. Must be one of 'x', 'y' or 'z'.
  721. modifier : int
  722. Scaling value.
  723. Raises
  724. ------
  725. ValueError
  726. If invalid `axis` is passed. Must be one of 'x', 'y','z'.
  727. RuntimeError
  728. If same gradient event is used on multiple axes.
  729. """
  730. if axis not in ["x", "y", "z"]:
  731. raise ValueError(
  732. f"Invalid axis. Must be one of 'x', 'y','z'. Passed: {axis}"
  733. )
  734. channel_num = ["x", "y", "z"].index(axis)
  735. other_channels = [0, 1, 2]
  736. other_channels.remove(channel_num)
  737. # Go through all event table entries and list gradient objects in the library
  738. all_grad_events = np.array(list(self.block_events.values()))
  739. all_grad_events = all_grad_events[:, 2:5]
  740. selected_events = np.unique(all_grad_events[:, channel_num])
  741. selected_events = selected_events[selected_events != 0]
  742. other_events = np.unique(all_grad_events[:, other_channels])
  743. if len(np.intersect1d(selected_events, other_events)) > 0:
  744. raise RuntimeError(
  745. "mod_grad_axis does not yet support the same gradient event used on multiple axes."
  746. )
  747. for i in range(len(selected_events)):
  748. self.grad_library.data[selected_events[i]][0] *= modifier
  749. if (
  750. self.grad_library.type[selected_events[i]] == "g"
  751. and self.grad_library.lengths[selected_events[i]] == 5
  752. ):
  753. # Need to update first and last fields
  754. self.grad_library.data[selected_events[i]][3] *= modifier
  755. self.grad_library.data[selected_events[i]][4] *= modifier
  756. def plot(
  757. self,
  758. label: str = str(),
  759. show_blocks: bool = False,
  760. save: bool = False,
  761. time_range=(0, np.inf),
  762. time_disp: str = "s",
  763. grad_disp: str = "kHz/m",
  764. plot_now: bool = True
  765. ) -> None:
  766. """
  767. Plot `Sequence`.
  768. Parameters
  769. ----------
  770. label : str, defualt=str()
  771. Plot label values for ADC events: in this example for LIN and REP labels; other valid labes are accepted as
  772. a comma-separated list.
  773. save : bool, default=False
  774. Boolean flag indicating if plots should be saved. The two figures will be saved as JPG with numerical
  775. suffixes to the filename 'seq_plot'.
  776. show_blocks : bool, default=False
  777. Boolean flag to indicate if grid and tick labels at the block boundaries are to be plotted.
  778. time_range : iterable, default=(0, np.inf)
  779. Time range (x-axis limits) for plotting the sequence. Default is 0 to infinity (entire sequence).
  780. time_disp : str, default='s'
  781. Time display type, must be one of `s`, `ms` or `us`.
  782. grad_disp : str, default='s'
  783. Gradient display unit, must be one of `kHz/m` or `mT/m`.
  784. plot_now : bool, default=True
  785. If true, function immediately shows the plots, blocking the rest of the code until plots are exited.
  786. If false, plots are shown when plt.show() is called. Useful if plots are to be modified.
  787. plot_type : str, default='Gradient'
  788. Gradients display type, must be one of either 'Gradient' or 'Kspace'.
  789. """
  790. mpl.rcParams["lines.linewidth"] = 0.75 # Set default Matplotlib linewidth
  791. valid_time_units = ["s", "ms", "us"]
  792. valid_grad_units = ["kHz/m", "mT/m"]
  793. valid_labels = get_supported_labels()
  794. if (
  795. not all([isinstance(x, (int, float)) for x in time_range])
  796. or len(time_range) != 2
  797. ):
  798. raise ValueError("Invalid time range")
  799. if time_disp not in valid_time_units:
  800. raise ValueError("Unsupported time unit")
  801. if grad_disp not in valid_grad_units:
  802. raise ValueError(
  803. "Unsupported gradient unit. Supported gradient units are: "
  804. + str(valid_grad_units)
  805. )
  806. fig1, fig2 = plt.figure(1), plt.figure(2)
  807. sp11 = fig1.add_subplot(311)
  808. sp12 = fig1.add_subplot(312, sharex=sp11)
  809. sp13 = fig1.add_subplot(313, sharex=sp11)
  810. fig2_subplots = [
  811. fig2.add_subplot(311, sharex=sp11),
  812. fig2.add_subplot(312, sharex=sp11),
  813. fig2.add_subplot(313, sharex=sp11),
  814. ]
  815. t_factor_list = [1, 1e3, 1e6]
  816. t_factor = t_factor_list[valid_time_units.index(time_disp)]
  817. g_factor_list = [1e-3, 1e3 / self.system.gamma]
  818. g_factor = g_factor_list[valid_grad_units.index(grad_disp)]
  819. t0 = 0
  820. label_defined = False
  821. label_idx_to_plot = []
  822. label_legend_to_plot = []
  823. label_store = dict()
  824. for i in range(len(valid_labels)):
  825. label_store[valid_labels[i]] = 0
  826. if valid_labels[i] in label.upper():
  827. label_idx_to_plot.append(i)
  828. label_legend_to_plot.append(valid_labels[i])
  829. if len(label_idx_to_plot) != 0:
  830. p = parula.main(len(label_idx_to_plot) + 1)
  831. label_colors_to_plot = p(np.arange(len(label_idx_to_plot)))
  832. label_colors_to_plot = np.roll(label_colors_to_plot, -1, axis=0).tolist()
  833. # Block timings
  834. block_edges = np.cumsum([0, *self.block_durations])
  835. block_edges_in_range = block_edges[
  836. (block_edges >= time_range[0]) * (block_edges <= time_range[1])
  837. ]
  838. if show_blocks:
  839. for sp in [sp11, sp12, sp13, *fig2_subplots]:
  840. sp.set_xticks(t_factor * block_edges_in_range)
  841. sp.set_xticklabels(rotation=90)
  842. for block_counter in range(len(self.block_events)):
  843. block = self.get_block(block_counter + 1)
  844. is_valid = time_range[0] <= t0 <= time_range[1]
  845. if is_valid:
  846. if getattr(block, "label", None) is not None:
  847. for i in range(len(block.label)):
  848. if block.label[i].type == "labelinc":
  849. label_store[block.label[i].label] += block.label[i].value
  850. else:
  851. label_store[block.label[i].label] = block.label[i].value
  852. label_defined = True
  853. if getattr(block, "adc", None) is not None: # ADC
  854. adc = block.adc
  855. # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this
  856. # is the present convention - the samples are shifted by 0.5 dwell
  857. t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell
  858. sp11.plot(t_factor * (t0 + t), np.zeros(len(t)), "rx")
  859. sp13.plot(
  860. t_factor * (t0 + t),
  861. np.angle(
  862. np.exp(1j * adc.phase_offset)
  863. * np.exp(1j * 2 * np.pi * t * adc.freq_offset)
  864. ),
  865. "b.",
  866. markersize=0.25,
  867. )
  868. if label_defined and len(label_idx_to_plot) != 0:
  869. cycler = mpl.cycler(color=label_colors_to_plot)
  870. sp11.set_prop_cycle(cycler)
  871. label_colors_to_plot = np.roll(
  872. label_colors_to_plot, -1, axis=0
  873. ).tolist()
  874. arr_label_store = list(label_store.values())
  875. lbl_vals = np.take(arr_label_store, label_idx_to_plot)
  876. t = t0 + adc.delay + (adc.num_samples - 1) / 2 * adc.dwell
  877. _t = [t_factor * t] * len(lbl_vals)
  878. # Plot each label individually to retrieve each corresponding Line2D object
  879. p = itertools.chain.from_iterable(
  880. [
  881. sp11.plot(__t, _lbl_vals, ".")
  882. for __t, _lbl_vals in zip(_t, lbl_vals)
  883. ]
  884. )
  885. if len(label_legend_to_plot) != 0:
  886. sp11.legend(p, label_legend_to_plot, loc="upper left")
  887. label_legend_to_plot = []
  888. if getattr(block, "rf", None) is not None: # RF
  889. rf = block.rf
  890. tc, ic = calc_rf_center(rf)
  891. time = rf.t
  892. signal = rf.signal
  893. if np.abs(signal[0]) != 0:
  894. signal = np.insert(signal, obj=0, values=0)
  895. time = np.insert(time, obj=0, values=time[0])
  896. ic += 1
  897. if np.abs(signal[-1]) != 0:
  898. signal = np.append(signal, 0)
  899. time = np.append(time, time[-1])
  900. sp12.plot(t_factor * (t0 + time + rf.delay), np.abs(signal))
  901. sp13.plot(
  902. t_factor * (t0 + time + rf.delay),
  903. np.angle(
  904. signal
  905. * np.exp(1j * rf.phase_offset)
  906. * np.exp(1j * 2 * math.pi * time * rf.freq_offset)
  907. ),
  908. t_factor * (t0 + tc + rf.delay),
  909. np.angle(
  910. signal[ic]
  911. * np.exp(1j * rf.phase_offset)
  912. * np.exp(1j * 2 * math.pi * time[ic] * rf.freq_offset)
  913. ),
  914. "xb",
  915. )
  916. grad_channels = ["gx", "gy", "gz"]
  917. for x in range(len(grad_channels)): # Gradients
  918. if getattr(block, grad_channels[x], None) is not None:
  919. grad = getattr(block, grad_channels[x])
  920. if grad.type == "grad":
  921. # We extend the shape by adding the first and the last points in an effort of making the
  922. # display a bit less confusing...
  923. time = grad.delay + [0, *grad.tt, grad.shape_dur]
  924. waveform = g_factor * np.array(
  925. (grad.first, *grad.waveform, grad.last)
  926. )
  927. else:
  928. time = np.cumsum(
  929. [
  930. 0,
  931. grad.delay,
  932. grad.rise_time,
  933. grad.flat_time,
  934. grad.fall_time,
  935. ]
  936. )
  937. waveform = (
  938. g_factor * grad.amplitude * np.array([0, 0, 1, 1, 0])
  939. )
  940. fig2_subplots[x].plot(t_factor * (t0 + time), waveform)
  941. t0 += self.block_durations[block_counter]
  942. grad_plot_labels = ["x", "y", "z"]
  943. sp11.set_ylabel("ADC")
  944. sp12.set_ylabel("RF mag (Hz)")
  945. sp13.set_ylabel("RF/ADC phase (rad)")
  946. sp13.set_xlabel(f"t ({time_disp})")
  947. for x in range(3):
  948. _label = grad_plot_labels[x]
  949. fig2_subplots[x].set_ylabel(f"G{_label} ({grad_disp})")
  950. fig2_subplots[-1].set_xlabel(f"t ({time_disp})")
  951. # Setting display limits
  952. disp_range = t_factor * np.array([time_range[0], min(t0, time_range[1])])
  953. [x.set_xlim(disp_range) for x in [sp11, sp12, sp13, *fig2_subplots]]
  954. # Grid on
  955. for sp in [sp11, sp12, sp13, *fig2_subplots]:
  956. sp.grid()
  957. fig1.tight_layout()
  958. fig2.tight_layout()
  959. if save:
  960. fig1.savefig("seq_plot1.jpg")
  961. fig2.savefig("seq_plot2.jpg")
  962. if plot_now:
  963. plt.show()
  964. def ppval_numpy(self, arr_np_poly: np.ndarray, xq: np.ndarray):
  965. """
  966. Perform piece-wise polynomial evaluation on Numpy's polyfit objects.
  967. Parameters
  968. ==========
  969. arr_np_poly : Iterable[np.poly1d]
  970. xq : np.array
  971. Returns
  972. =======
  973. """
  974. result = []
  975. result2 = []
  976. breaks = np.array([p.domain for p in arr_np_poly])
  977. breaks = np.array([*breaks[::2].flatten(), breaks[-1, -1]])
  978. last_match = 0
  979. for x in xq:
  980. # Evaluate polynomial only when we find x's corresponding pp window
  981. for i in range(last_match, len(arr_np_poly)):
  982. corresponding_pp = x > arr_np_poly[i].domain
  983. if corresponding_pp.tolist() == [True, False]:
  984. value = np.polynomial.polynomial.polyval(x, arr_np_poly[i].coef)
  985. result.append(value)
  986. last_match = i
  987. break
  988. corresponding_pp2 = np.nonzero((x < breaks))[0][0]
  989. value2 = np.polynomial.polynomial.polyval(
  990. x, arr_np_poly[corresponding_pp2].coef
  991. )
  992. result2.append(value2)
  993. return result
  994. def ppval_MATLAB(self, arr_MATLAB_poly: np.ndarray, xq: np.ndarray) -> np.array:
  995. """
  996. Perform piece-wise polynomial evaluation on MATLAB's polyfit objects.
  997. Returns
  998. =======
  999. """
  1000. def __ppval(c, x, b, i, n):
  1001. return c[n] * (x - b) ** i + __ppval(c, x, b, i + 1, n - 1) if n >= 0 else 0
  1002. coefs = np.array([p.coef for p in arr_MATLAB_poly])
  1003. breaks = np.array([p.domain for p in arr_MATLAB_poly])
  1004. breaks = np.array([*breaks[::2].flatten(), breaks[-1, -1]])
  1005. result = []
  1006. for x in xq:
  1007. corresponding_pp = np.nonzero((x < breaks))[0][0]
  1008. c = coefs[corresponding_pp - 1]
  1009. b = breaks[corresponding_pp - 1]
  1010. # c = coefs[2]
  1011. # res = c[0] * (x - breaks[2]) ** 2 + c[1] * (x - breaks[2]) + c[2]
  1012. res = __ppval(c, x, b, i=0, n=len(c) - 1)
  1013. result.append(res)
  1014. return np.array(result)
  1015. def read(self, file_path: str, detect_rf_use: bool = False) -> None:
  1016. """
  1017. Read `.seq` file from `file_path`.
  1018. Parameters
  1019. ----------
  1020. detect_rf_use
  1021. file_path : str
  1022. Path to `.seq` file to be read.
  1023. """
  1024. read(self, path=file_path, detect_rf_use=detect_rf_use)
  1025. def register_adc_event(self, event: EventLibrary) -> int:
  1026. return block.register_adc_event(self, event)
  1027. def register_grad_event(
  1028. self, event: SimpleNamespace
  1029. ) -> Union[int, Tuple[int, int]]:
  1030. return block.register_grad_event(self, event)
  1031. def register_label_event(self, event: SimpleNamespace) -> int:
  1032. return block.register_label_event(self, event)
  1033. def register_rf_event(self, event: SimpleNamespace) -> Tuple[int, List[int]]:
  1034. return block.register_rf_event(self, event)
  1035. def rf_from_lib_data(self, lib_data: list, use: str = str()) -> SimpleNamespace:
  1036. """
  1037. Construct RF object from `lib_data`.
  1038. Parameters
  1039. ----------
  1040. lib_data : list
  1041. RF envelope.
  1042. use : str, default=str()
  1043. RF event use.
  1044. Returns
  1045. -------
  1046. rf : SimpleNamespace
  1047. RF object constructed from `lib_data`.
  1048. """
  1049. rf = SimpleNamespace()
  1050. rf.type = "rf"
  1051. amplitude, mag_shape, phase_shape = lib_data[0], lib_data[1], lib_data[2]
  1052. shape_data = self.shape_library.data[mag_shape]
  1053. compressed = SimpleNamespace()
  1054. compressed.num_samples = shape_data[0]
  1055. compressed.data = shape_data[1:]
  1056. mag = decompress_shape(compressed)
  1057. shape_data = self.shape_library.data[phase_shape]
  1058. compressed.num_samples = shape_data[0]
  1059. compressed.data = shape_data[1:]
  1060. phase = decompress_shape(compressed)
  1061. rf.signal = amplitude * mag * np.exp(1j * 2 * np.pi * phase)
  1062. time_shape = lib_data[3]
  1063. if time_shape > 0:
  1064. shape_data = self.shape_library.data[time_shape]
  1065. compressed.num_samples = shape_data[0]
  1066. compressed.data = shape_data[1:]
  1067. rf.t = decompress_shape(compressed) * self.rf_raster_time
  1068. rf.shape_dur = (
  1069. np.ceil((rf.t[-1] - eps) / self.rf_raster_time) * self.rf_raster_time
  1070. )
  1071. else: # Generate default time raster on the fly
  1072. rf.t = (np.arange(1, len(rf.signal) + 1) - 0.5) * self.rf_raster_time
  1073. rf.shape_dur = len(rf.signal) * self.rf_raster_time
  1074. rf.delay = lib_data[4]
  1075. rf.freq_offset = lib_data[5]
  1076. rf.phase_offset = lib_data[6]
  1077. rf.dead_time = self.system.rf_dead_time
  1078. rf.ringdown_time = self.system.rf_ringdown_time
  1079. if use != "":
  1080. use_cases = {
  1081. "e": "excitation",
  1082. "r": "refocusing",
  1083. "i": "inversion",
  1084. "s": "saturation",
  1085. "p": "preparation",
  1086. }
  1087. rf.use = use_cases[use] if use in use_cases else "undefined"
  1088. return rf
  1089. def set_block(self, block_index: int, *args: SimpleNamespace) -> None:
  1090. """
  1091. Replace block at index with new block provided as block structure, add sequence block, or create a new block
  1092. from events and store at position specified by index. The block or events are provided in uncompressed form and
  1093. will be stored in the compressed, non-redundant internal libraries.
  1094. See also:
  1095. - `pypulseq.Sequence.sequence.Sequence.get_block()`
  1096. - `pypulseq.Sequence.sequence.Sequence.add_block()`
  1097. Parameters
  1098. ----------
  1099. block_index : int
  1100. Index at which block is replaced.
  1101. args : SimpleNamespace
  1102. Block or events to be replaced/added or created at `block_index`.
  1103. """
  1104. block.set_block(self, block_index, *args)
  1105. def set_definition(
  1106. self, key: str, value: Union[float, int, list, np.ndarray, str, tuple]
  1107. ) -> None:
  1108. """
  1109. Modify a custom definition of the sequence. Set the user definition 'key' to value 'value'. If the definition
  1110. does not exist it will be created.
  1111. See also `pypulseq.Sequence.sequence.Sequence.get_definition()`.
  1112. Parameters
  1113. ----------
  1114. key : str
  1115. Definition key.
  1116. value : int, list, np.ndarray, str or tuple
  1117. Definition value.
  1118. """
  1119. if key == "FOV":
  1120. if np.max(value) > 1:
  1121. text = "Definition FOV uses values exceeding 1 m. "
  1122. text += "New Pulseq interpreters expect values in units of meters."
  1123. warn(text)
  1124. self.definitions[key] = value
  1125. def set_extension_string_ID(self, extension_str: str, extension_id: int) -> None:
  1126. """
  1127. Set numeric ID for the given string extension ID.
  1128. Parameters
  1129. ----------
  1130. extension_str : str
  1131. Given string extension ID.
  1132. extension_id : int
  1133. Given numeric extension ID.
  1134. Raises
  1135. ------
  1136. ValueError
  1137. If given numeric or string extension ID is not unique.
  1138. """
  1139. if (
  1140. extension_str in self.extension_string_idx
  1141. or extension_id in self.extension_numeric_idx
  1142. ):
  1143. raise ValueError("Numeric or string ID is not unique")
  1144. self.extension_numeric_idx.append(extension_id)
  1145. self.extension_string_idx.append(extension_str)
  1146. assert len(self.extension_numeric_idx) == len(self.extension_string_idx)
  1147. def test_report(self) -> str:
  1148. """
  1149. Analyze the sequence and return a text report.
  1150. """
  1151. return ext_test_report(self)
  1152. def waveforms_and_times(
  1153. self, append_RF: bool = False
  1154. ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
  1155. """
  1156. Decompress the entire gradient waveform. Returns gradient waveforms as a tuple of `np.ndarray` of
  1157. `gradient_axes` (typically 3) dimensions. Each `np.ndarray` contains timepoints and the corresponding
  1158. gradient amplitude values. Additional return values are time points of excitations, refocusings and ADC
  1159. sampling points.
  1160. Parameters
  1161. ----------
  1162. append_RF : bool, default=False
  1163. Boolean flag to indicate if RF wave shapes are to be appended after the gradients.
  1164. Returns
  1165. -------
  1166. wave_data : np.ndarray
  1167. tfp_excitation : np.ndarray
  1168. Contains time moments, frequency and phase offsets of the excitation RF pulses (similar for `
  1169. tfp_refocusing`).
  1170. tfp_refocusing : np.ndarray
  1171. t_adc: np.ndarray
  1172. Contains times of all ADC sample points.
  1173. fp_adc : np.ndarray
  1174. Contains frequency and phase offsets of each ADC object (not samples).
  1175. """
  1176. grad_channels = ["gx", "gy", "gz"]
  1177. num_blocks = len(self.block_events)
  1178. # Collect shape pieces
  1179. if append_RF:
  1180. shape_channels = len(grad_channels) + 1 # Last 'channel' is RF
  1181. else:
  1182. shape_channels = len(grad_channels)
  1183. shape_pieces = np.empty((shape_channels, num_blocks), dtype="object")
  1184. # Also collect RF and ADC timing data
  1185. # t_excitation, t_refocusing, t_adc
  1186. tfp_excitation = []
  1187. tfp_refocusing = []
  1188. t_adc = []
  1189. fp_adc = []
  1190. curr_dur = 0
  1191. out_len = np.zeros(shape_channels) # Last 'channel' is RF
  1192. for block_counter in range(num_blocks):
  1193. block = self.get_block(block_counter + 1)
  1194. for j in range(len(grad_channels)):
  1195. grad = getattr(block, grad_channels[j])
  1196. if grad is not None: # Gradients
  1197. if grad.type == "grad":
  1198. # Check if we have an extended trapezoid or an arbitrary gradient on a regular raster
  1199. tt_rast = grad.tt / self.grad_raster_time + 0.5
  1200. if np.all(
  1201. np.abs(tt_rast - np.arange(1, len(tt_rast) + 1)) < eps
  1202. ): # Arbitrary gradient
  1203. """
  1204. Arbitrary gradient: restore & recompress shape - if we had a trapezoid converted to shape we
  1205. have to find the "corners" and we can eliminate internal samples on the straight segments
  1206. but first we have to restore samples on the edges of the gradient raster intervals for that
  1207. we need the first sample.
  1208. """
  1209. max_abs = np.max(np.abs(grad.waveform))
  1210. odd_step1 = np.array([grad.first, *(2 * grad.waveform)])
  1211. odd_step2 = odd_step1 * (
  1212. np.mod(np.arange(1, len(odd_step1) + 1), 2) * 2 - 1
  1213. )
  1214. waveform_odd_rest = np.cumsum(odd_step2) * (
  1215. np.mod(np.arange(1, len(odd_step2)), 2) * 2 - 1
  1216. )
  1217. else: # Extended trapezoid
  1218. out_len[j] += len(grad.tt)
  1219. shape_pieces[j, block_counter] = np.array(
  1220. [
  1221. curr_dur + grad.delay + grad.tt,
  1222. grad.waveform,
  1223. ]
  1224. )
  1225. else:
  1226. if np.abs(grad.flat_time) > eps:
  1227. out_len[j] += 4
  1228. _temp = np.vstack(
  1229. (
  1230. curr_dur
  1231. + grad.delay
  1232. + np.cumsum(
  1233. [
  1234. 0,
  1235. grad.rise_time,
  1236. grad.flat_time,
  1237. grad.fall_time,
  1238. ]
  1239. ),
  1240. grad.amplitude * np.array([0, 1, 1, 0]),
  1241. )
  1242. )
  1243. shape_pieces[j, block_counter] = _temp
  1244. else:
  1245. out_len[j] += 3
  1246. _temp = np.vstack(
  1247. (
  1248. curr_dur
  1249. + grad.delay
  1250. + np.cumsum([0, grad.rise_time, grad.fall_time]),
  1251. grad.amplitude * np.array([0, 1, 0]),
  1252. )
  1253. )
  1254. shape_pieces[j, block_counter] = _temp
  1255. if block.rf is not None: # RF
  1256. rf = block.rf
  1257. t = rf.delay + calc_rf_center(rf)[0]
  1258. if not hasattr(rf, "use") or block.rf.use in [
  1259. "excitation",
  1260. "undefined",
  1261. ]:
  1262. tfp_excitation.append(
  1263. [curr_dur + t, block.rf.freq_offset, block.rf.phase_offset]
  1264. )
  1265. elif block.rf.use == "refocusing":
  1266. tfp_refocusing.append(
  1267. [curr_dur + t, block.rf.freq_offset, block.rf.phase_offset]
  1268. )
  1269. if append_RF:
  1270. rf_piece = np.array(
  1271. [
  1272. curr_dur + rf.delay + rf.t,
  1273. rf.signal
  1274. * np.exp(
  1275. 1j
  1276. * (rf.phase_offset + 2 * np.pi * rf.freq_offset * rf.t)
  1277. ),
  1278. ]
  1279. )
  1280. out_len[-1] += len(rf.t)
  1281. if np.abs(rf.signal[0]) > 0:
  1282. pre = np.array([[curr_dur + rf.delay + rf.t[0] - eps], [0]])
  1283. rf_piece = np.hstack((pre, rf_piece))
  1284. out_len[-1] += pre.shape[1]
  1285. if np.abs(rf.signal[-1]) > 0:
  1286. post = np.array([[curr_dur + rf.delay + rf.t[-1] + eps], [0]])
  1287. rf_piece = np.hstack((rf_piece, post))
  1288. out_len[-1] += post.shape[1]
  1289. shape_pieces[-1, block_counter] = rf_piece
  1290. if block.adc is not None: # ADC
  1291. t_adc.extend(
  1292. np.arange(block.adc.num_samples) * block.adc.dwell
  1293. + 0.5 * block.adc.dwell
  1294. + block.adc.delay
  1295. + curr_dur
  1296. )
  1297. fp_adc.append([block.adc.freq_offset, block.adc.phase_offset])
  1298. curr_dur += self.block_durations[block_counter]
  1299. # Collect wave data
  1300. wave_data = np.empty(shape_channels, dtype="object")
  1301. for j in range(shape_channels):
  1302. wave_data[j] = np.zeros((2, int(out_len[j])))
  1303. # TODO: This is weird, and needs to be fixed. Time points are also complex this way, and spends 4 times more memory than necessary.
  1304. if append_RF:
  1305. wave_data[j] = np.zeros((2, int(out_len[j])), dtype=np.complex128)
  1306. wave_cnt = np.zeros(shape_channels, dtype=int)
  1307. for block_counter in range(num_blocks):
  1308. for j in range(shape_channels):
  1309. if shape_pieces[j, block_counter] is not None:
  1310. wave_data_local = shape_pieces[j, block_counter]
  1311. length = wave_data_local.shape[1]
  1312. if (
  1313. wave_cnt[j] == 0
  1314. or wave_data[j][0, wave_cnt[j] - 1] != wave_data_local[0, 0]
  1315. ):
  1316. wave_data[j][
  1317. :, wave_cnt[j] + np.arange(length)
  1318. ] = wave_data_local
  1319. wave_cnt[j] += length
  1320. else:
  1321. wave_data[j][
  1322. :, wave_cnt[j] + np.arange(length - 1)
  1323. ] = wave_data_local[:, 1:]
  1324. wave_cnt[j] += length - 1
  1325. if wave_cnt[j] != len(np.unique(wave_data[j][0, : wave_cnt[j]])):
  1326. raise Warning(
  1327. "Not all elements of the generated time vector are unique."
  1328. )
  1329. # Trim output data
  1330. for j in range(shape_channels):
  1331. if wave_cnt[j] < wave_data[j].shape[1]:
  1332. wave_data[j] = wave_data[j][:, : wave_cnt[j]]
  1333. tfp_excitation = np.array(tfp_excitation).transpose()
  1334. tfp_refocusing = np.array(tfp_refocusing)
  1335. t_adc = np.array(t_adc)
  1336. fp_adc = np.array(fp_adc)
  1337. return wave_data, tfp_excitation, tfp_refocusing, t_adc, fp_adc
  1338. def waveforms_export(self, time_range=(0, np.inf)) -> dict:
  1339. """
  1340. Plot `Sequence`.
  1341. Parameters
  1342. ----------
  1343. time_range : iterable, default=(0, np.inf)
  1344. Time range (x-axis limits) for all waveforms. Default is 0 to infinity (entire sequence).
  1345. Returns
  1346. -------
  1347. all_waveforms: dict
  1348. Dictionary containing the following sequence waveforms and time array(s):
  1349. - `t_adc` - ADC timing array [seconds]
  1350. - `t_rf` - RF timing array [seconds]
  1351. - `t_rf_centers`: `rf_t_centers`,
  1352. - `t_gx`: x gradient timing array,
  1353. - `t_gy`: y gradient timing array,
  1354. - `t_gz`: z gradient timing array,
  1355. - `adc` - ADC complex signal (amplitude=1, phase=adc phase) [a.u.]
  1356. - `rf` - RF complex signal
  1357. - `rf_centers`: RF centers array,
  1358. - `gx` - x gradient
  1359. - `gy` - y gradient
  1360. - `gz` - z gradient
  1361. - `grad_unit`: [kHz/m],
  1362. - `rf_unit`: [Hz],
  1363. - `time_unit`: [seconds],
  1364. """
  1365. # Check time range validity
  1366. if (
  1367. not all([isinstance(x, (int, float)) for x in time_range])
  1368. or len(time_range) != 2
  1369. ):
  1370. raise ValueError("Invalid time range")
  1371. t0 = 0
  1372. adc_t_all = np.array([])
  1373. adc_signal_all = np.array([], dtype=complex)
  1374. rf_t_all = np.array([])
  1375. rf_signal_all = np.array([], dtype=complex)
  1376. rf_t_centers = np.array([])
  1377. rf_signal_centers = np.array([], dtype=complex)
  1378. gx_t_all = np.array([])
  1379. gy_t_all = np.array([])
  1380. gz_t_all = np.array([])
  1381. gx_all = np.array([])
  1382. gy_all = np.array([])
  1383. gz_all = np.array([])
  1384. for block_counter in range(len(self.dict_block_events)): # For each block
  1385. block = self.get_block(block_counter + 1) # Retrieve it
  1386. is_valid = (
  1387. time_range[0] <= t0 <= time_range[1]
  1388. ) # Check if "current time" is within requested range.
  1389. if is_valid:
  1390. # Case 1: ADC
  1391. if hasattr(block, "adc"):
  1392. adc = block.adc # Get adc info
  1393. # From Pulseq: According to the information from Klaus Scheffler and indirectly from Siemens this
  1394. # is the present convention - the samples are shifted by 0.5 dwell
  1395. t = adc.delay + (np.arange(int(adc.num_samples)) + 0.5) * adc.dwell
  1396. adc_t = t0 + t
  1397. adc_signal = np.exp(1j * adc.phase_offset) * np.exp(
  1398. 1j * 2 * np.pi * t * adc.freq_offset
  1399. )
  1400. adc_t_all = np.append(adc_t_all, adc_t)
  1401. adc_signal_all = np.append(adc_signal_all, adc_signal)
  1402. if hasattr(block, "rf"):
  1403. rf = block.rf
  1404. tc, ic = calc_rf_center(rf)
  1405. t = rf.t + rf.delay
  1406. tc = tc + rf.delay
  1407. # Debug - visualize
  1408. # sp12.plot(t_factor * (t0 + t), np.abs(rf.signal))
  1409. # sp13.plot(t_factor * (t0 + t), np.angle(rf.signal * np.exp(1j * rf.phase_offset)
  1410. # * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)),
  1411. # t_factor * (t0 + tc), np.angle(rf.signal[ic] * np.exp(1j * rf.phase_offset)
  1412. # * np.exp(1j * 2 * math.pi * rf.t[ic] * rf.freq_offset)),
  1413. # 'xb')
  1414. rf_t = t0 + t
  1415. rf = (
  1416. rf.signal
  1417. * np.exp(1j * rf.phase_offset)
  1418. * np.exp(1j * 2 * math.pi * rf.t * rf.freq_offset)
  1419. )
  1420. rf_t_all = np.append(rf_t_all, rf_t)
  1421. rf_signal_all = np.append(rf_signal_all, rf)
  1422. rf_t_centers = np.append(rf_t_centers, rf_t[ic])
  1423. rf_signal_centers = np.append(rf_signal_centers, rf[ic])
  1424. grad_channels = ["gx", "gy", "gz"]
  1425. for x in range(
  1426. len(grad_channels)
  1427. ): # Check each gradient channel: x, y, and z
  1428. if hasattr(
  1429. block, grad_channels[x]
  1430. ): # If this channel is on in current block
  1431. grad = getattr(block, grad_channels[x])
  1432. if grad.type == "grad": # Arbitrary gradient option
  1433. # In place unpacking of grad.t with the starred expression
  1434. g_t = (
  1435. t0
  1436. + grad.delay
  1437. + [
  1438. 0,
  1439. *(grad.t + (grad.t[1] - grad.t[0]) / 2),
  1440. grad.t[-1] + grad.t[1] - grad.t[0],
  1441. ]
  1442. )
  1443. g = 1e-3 * np.array((grad.first, *grad.waveform, grad.last))
  1444. else: # Trapezoid gradient option
  1445. g_t = t0 + np.cumsum(
  1446. [
  1447. 0,
  1448. grad.delay,
  1449. grad.rise_time,
  1450. grad.flat_time,
  1451. grad.fall_time,
  1452. ]
  1453. )
  1454. g = 1e-3 * grad.amplitude * np.array([0, 0, 1, 1, 0])
  1455. if grad.channel == "x":
  1456. gx_t_all = np.append(gx_t_all, g_t)
  1457. gx_all = np.append(gx_all, g)
  1458. elif grad.channel == "y":
  1459. gy_t_all = np.append(gy_t_all, g_t)
  1460. gy_all = np.append(gy_all, g)
  1461. elif grad.channel == "z":
  1462. gz_t_all = np.append(gz_t_all, g_t)
  1463. gz_all = np.append(gz_all, g)
  1464. t0 += self.arr_block_durations[
  1465. block_counter
  1466. ] # "Current time" gets updated to end of block just examined
  1467. all_waveforms = {
  1468. "t_adc": adc_t_all,
  1469. "t_rf": rf_t_all,
  1470. "t_rf_centers": rf_t_centers,
  1471. "t_gx": gx_t_all,
  1472. "t_gy": gy_t_all,
  1473. "t_gz": gz_t_all,
  1474. "adc": adc_signal_all,
  1475. "rf": rf_signal_all,
  1476. "rf_centers": rf_signal_centers,
  1477. "gx": gx_all,
  1478. "gy": gy_all,
  1479. "gz": gz_all,
  1480. "grad_unit": "[kHz/m]",
  1481. "rf_unit": "[Hz]",
  1482. "time_unit": "[seconds]",
  1483. }
  1484. return all_waveforms
  1485. def write(self, name: str, create_signature: bool = True) -> None:
  1486. """
  1487. Write the sequence data to the given filename using the open file format for MR sequences.
  1488. See also `pypulseq.Sequence.read_seq.read()`.
  1489. Parameters
  1490. ----------
  1491. name : str
  1492. Filename of `.seq` file to be written to disk.
  1493. create_signature : bool, default=True
  1494. Boolean flag to indicate if the file has to be signed.
  1495. """
  1496. write_seq(self, name, create_signature)