write_ute.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. """
  2. A very basic UTE-like sequence, without ramp-sampling, ramp-RF. Achieves TE in the range of 300-400 us
  3. """
  4. from copy import copy
  5. import numpy as np
  6. from matplotlib import pyplot as plt
  7. import pypulseq as pp
  8. def main(plot: bool, write_seq: bool, seq_filename: str = "ute_pypulseq.seq"):
  9. # ======
  10. # SETUP
  11. # ======
  12. seq = pp.Sequence() # Create a new sequence object
  13. fov = 250e-3 # Define FOV and resolution
  14. Nx = 256
  15. alpha = 10 # Flip angle
  16. slice_thickness = 3e-3 # Slice thickness
  17. TR = 10e-3 # Repetition tme
  18. Nr = 128 # Number of radial spokes
  19. delta = 2 * np.pi / Nr # Angular increment
  20. ro_duration = 2.56e-3 # Read-out time: controls RO bandwidth and T2-blurring
  21. ro_os = 2 # Oversampling
  22. ro_asymmetry = 1 # 0: Fully symmetric; 1: half-echo
  23. rf_spoiling_inc = 117 # RF spoiling increment
  24. # Set system limits
  25. system = pp.Opts(
  26. max_grad=28,
  27. grad_unit="mT/m",
  28. max_slew=100,
  29. slew_unit="T/m/s",
  30. rf_ringdown_time=20e-6,
  31. rf_dead_time=100e-6,
  32. adc_dead_time=10e-6,
  33. )
  34. # ======
  35. # CREATE EVENTS
  36. # ======
  37. # Create alpha-degree slice selection pulse and gradient
  38. rf, gz, gz_reph = pp.make_sinc_pulse(
  39. flip_angle=alpha * np.pi / 180,
  40. duration=1e-3,
  41. slice_thickness=slice_thickness,
  42. apodization=0.5,
  43. time_bw_product=2,
  44. center_pos=1,
  45. system=system,
  46. return_gz=True,
  47. )
  48. # Align RO asymmetry to ADC samples
  49. Nxo = np.round(ro_os * Nx)
  50. ro_asymmetry = pp.round_half_up(ro_asymmetry * Nxo / 2) / Nxo * 2
  51. # Define other gradients and ADC events
  52. delta_k = 1 / fov / (1 + ro_asymmetry)
  53. ro_area = Nx * delta_k
  54. gx = pp.make_trapezoid(
  55. channel="x", flat_area=ro_area, flat_time=ro_duration, system=system
  56. )
  57. adc = pp.make_adc(
  58. num_samples=Nxo, duration=gx.flat_time, delay=gx.rise_time, system=system
  59. )
  60. gx_pre = pp.make_trapezoid(
  61. channel="x",
  62. area=-(gx.area - ro_area) / 2
  63. - gx.amplitude * adc.dwell / 2
  64. - ro_area / 2 * (1 - ro_asymmetry),
  65. system=system,
  66. )
  67. # Gradient spoiling
  68. gx_spoil = pp.make_trapezoid(channel="x", area=0.2 * Nx * delta_k, system=system)
  69. # Calculate timing
  70. TE = (
  71. gz.fall_time
  72. + pp.calc_duration(gx_pre, gz_reph)
  73. + gx.rise_time
  74. + adc.dwell * Nxo / 2 * (1 - ro_asymmetry)
  75. )
  76. delay_TR = (
  77. np.ceil(
  78. (
  79. TR
  80. - pp.calc_duration(gx_pre, gz_reph)
  81. - pp.calc_duration(gz)
  82. - pp.calc_duration(gx)
  83. )
  84. / seq.grad_raster_time
  85. )
  86. * seq.grad_raster_time
  87. )
  88. assert np.all(delay_TR >= pp.calc_duration(gx_spoil))
  89. print(f"TE = {TE * 1e6:.0f} us")
  90. if pp.calc_duration(gz_reph) > pp.calc_duration(gx_pre):
  91. gx_pre.delay = pp.calc_duration(gz_reph) - pp.calc_duration(gx_pre)
  92. rf_phase = 0
  93. rf_inc = 0
  94. # ======
  95. # CONSTRUCT SEQUENCE
  96. # ======
  97. for i in range(Nr):
  98. for c in range(2):
  99. rf.phase_offset = rf_phase / 180 * np.pi
  100. adc.phase_offset = rf_phase / 180 * np.pi
  101. rf_inc = np.mod(rf_inc + rf_spoiling_inc, 360.0)
  102. rf_phase = np.mod(rf_phase + rf_inc, 360.0)
  103. gz.amplitude = -gz.amplitude # Alternate GZ amplitude
  104. gz_reph.amplitude = -gz_reph.amplitude
  105. seq.add_block(rf, gz)
  106. phi = delta * i
  107. gpc = copy(gx_pre)
  108. gps = copy(gx_pre)
  109. gpc.amplitude = gx_pre.amplitude * np.cos(phi)
  110. gps.amplitude = gx_pre.amplitude * np.sin(phi)
  111. gps.channel = "y"
  112. grc = copy(gx)
  113. grs = copy(gx)
  114. grc.amplitude = gx.amplitude * np.cos(phi)
  115. grs.amplitude = gx.amplitude * np.sin(phi)
  116. grs.channel = "y"
  117. gsc = copy(gx_spoil)
  118. gss = copy(gx_spoil)
  119. gsc.amplitude = gx_spoil.amplitude * np.cos(phi)
  120. gss.amplitude = gx_spoil.amplitude * np.sin(phi)
  121. gss.channel = "y"
  122. seq.add_block(gpc, gps, gz_reph)
  123. seq.add_block(grc, grs, adc)
  124. seq.add_block(gsc, gss, pp.make_delay(delay_TR))
  125. # Check whether the timing of the sequence is correct
  126. ok, error_report = seq.check_timing()
  127. if ok:
  128. print("Timing check passed successfully")
  129. else:
  130. print("Timing check failed. Error listing follows:")
  131. [print(e) for e in error_report]
  132. # ======
  133. # VISUALIZATION
  134. # ======
  135. if plot:
  136. seq.plot()
  137. # Plot gradients to check for gaps and optimality of the timing
  138. gw = seq.waveforms_and_times()[0]
  139. # Plot the entire gradient shape
  140. plt.figure()
  141. plt.plot(gw[0][0], gw[0][1], gw[1][0], gw[1][1], gw[2][0], gw[2][1])
  142. plt.show()
  143. # =========
  144. # WRITE .SEQ
  145. # =========
  146. if write_seq:
  147. # Prepare the sequence output for the scanner
  148. seq.set_definition(key="FOV", value=[fov, fov, slice_thickness])
  149. seq.set_definition(key="Name", value="UTE")
  150. seq.write(seq_filename)
  151. if __name__ == "__main__":
  152. main(plot=True, write_seq=True)