make_adiabatic_pulse.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. from types import SimpleNamespace
  2. from typing import Tuple, Union
  3. import numpy as np
  4. from sigpy.mri.rf import hypsec, wurst
  5. from seqgen.pypulseq import eps
  6. from seqgen.pypulseq.calc_duration import calc_duration
  7. from seqgen.pypulseq.calc_rf_center import calc_rf_center
  8. from seqgen.pypulseq.make_delay import make_delay
  9. from seqgen.pypulseq.make_trapezoid import make_trapezoid
  10. from seqgen.pypulseq.opts import Opts
  11. from seqgen.pypulseq.supported_labels_rf_use import get_supported_rf_uses
  12. def make_adiabatic_pulse(
  13. pulse_type: str,
  14. adiabaticity: int = 4,
  15. bandwidth: int = 40000,
  16. beta: int = 800,
  17. delay: float = 0,
  18. duration: float = 10e-3,
  19. dwell: float = 0,
  20. freq_offset: float = 0,
  21. max_grad: float = 0,
  22. max_slew: float = 0,
  23. n_fac: int = 40,
  24. mu: float = 4.9,
  25. phase_offset: float = 0,
  26. return_gz: bool = False,
  27. return_delay: bool = False,
  28. slice_thickness: float = 0,
  29. system=Opts(),
  30. use: str = str(),
  31. ) -> Union[
  32. SimpleNamespace,
  33. Tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace, SimpleNamespace],
  34. Tuple[SimpleNamespace, SimpleNamespace, SimpleNamespace],
  35. ]:
  36. """
  37. Make an adiabatic inversion pulse.
  38. Note: some parameters only affect certain pulse types and are ignored for other; e.g. bandwidth is ignored if
  39. type='hypsec'.
  40. hypsec(n=512, beta=800, mu=4.9, dur=0.012)
  41. Design a hyperbolic secant adiabatic pulse. `mu` * `beta` becomes the amplitude of the frequency sweep.
  42. Args:
  43. - n (int): number of samples (should be a multiple of 4).
  44. - beta (float): AM waveform parameter.
  45. - mu (float): a constant, determines amplitude of frequency sweep.
  46. - dur (float): pulse time (s).
  47. Returns:
  48. 2-element tuple containing
  49. - **a** (*array*): AM waveform.
  50. - **om** (*array*): FM waveform (radians/s).
  51. References:
  52. Baum, J., Tycko, R. and Pines, A. (1985). 'Broadband and adiabatic
  53. inversion of a two-level system by phase-modulated pulses'.
  54. Phys. Rev. A., 32:3435-3447.
  55. wurst(n=512, n_fac=40, bw=40000.0, dur=0.002)
  56. Design a WURST (wideband, uniform rate, smooth truncation) adiabatic inversion pulse
  57. Args:
  58. - n (int): number of samples (should be a multiple of 4).
  59. - n_fac (int): power to exponentiate to within AM term. ~20 or greater is typical.
  60. - bw (float): pulse bandwidth.
  61. - dur (float): pulse time (s).
  62. Returns:
  63. 2-element tuple containing
  64. - **a** (*array*): AM waveform.
  65. - **om** (*array*): FM waveform (radians/s).
  66. References:
  67. Kupce, E. and Freeman, R. (1995). 'Stretched Adiabatic Pulses for
  68. Broadband Spin Inversion'.
  69. J. Magn. Reson. Ser. A., 117:246-256.
  70. Parameters
  71. ----------
  72. pulse_type : str
  73. One of 'hypsec' or 'wurst' pulse types.
  74. adiabaticity : int, default=4
  75. bandwidth : int, default=40000
  76. Pulse bandwidth.
  77. beta : int, default=800
  78. AM waveform parameter.
  79. delay : float, default=0
  80. Delay in seconds (s).
  81. duration : float, default=10e-3
  82. Pulse time (s).
  83. dwell : float, default=0
  84. freq_offset : float, default=0
  85. max_grad : float, default=0
  86. Maximum gradient strength.
  87. max_slew : float, default=0
  88. Maximum slew rate.
  89. mu : float, default=4.9
  90. Constant determining amplitude of frequency sweep.
  91. n_fac : int, default=40
  92. Power to exponentiate to within AM term. ~20 or greater is typical.
  93. phase_offset : float, default=0
  94. Phase offset.
  95. return_delay : bool, default=False
  96. Boolean flag to indicate if the delay has to be returned.
  97. return_gz : bool, default=False
  98. Boolean flag to indicate if the slice-selective gradient has to be returned.
  99. slice_thickness : float, default=0
  100. system : Opts, default=Opts()
  101. System limits.
  102. use : str
  103. Whether it is a 'refocusing' pulse (for k-space calculation).
  104. Returns
  105. -------
  106. rf : SimpleNamespace
  107. Adiabatic RF pulse event.
  108. gz : SimpleNamespace, optional
  109. Slice-selective trapezoid event.
  110. gzr : SimpleNamespace, optional
  111. Slice-select rephasing trapezoid event.
  112. delay : SimpleNamespace, optional
  113. Delay event.
  114. Raises
  115. ------
  116. ValueError
  117. If invalid pulse type is encountered.
  118. If invalid pulse use is encountered.
  119. If slice thickness is not provided but slice-selective trapezoid event is expected.
  120. """
  121. valid_pulse_types = ["hypsec", "wurst"]
  122. if pulse_type != "" and pulse_type not in valid_pulse_types:
  123. raise ValueError(
  124. f"Invalid type parameter. Must be one of {valid_pulse_types}.Passed: {pulse_type}"
  125. )
  126. valid_pulse_uses = get_supported_rf_uses()
  127. if use != "" and use not in valid_pulse_uses:
  128. raise ValueError(
  129. f"Invalid use parameter. Must be one of {valid_pulse_uses}. Passed: {use}"
  130. )
  131. if dwell == 0:
  132. dwell = system.rf_raster_time
  133. n_raw = np.round(duration / dwell + eps)
  134. # Number of points must be divisible by 4 - requirement of sigpy.mri
  135. N = np.floor(n_raw / 4) * 4
  136. if pulse_type == "hypsec":
  137. am, fm = hypsec(n=N, beta=beta, mu=mu, dur=duration)
  138. elif pulse_type == "wurst":
  139. am, fm = wurst(n=N, n_fac=n_fac, bw=bandwidth, dur=duration)
  140. else:
  141. raise ValueError("Unsupported adiabatic pulse type.")
  142. pm = np.cumsum(fm) * dwell
  143. ifm = np.argmin(np.abs(fm))
  144. dfm = np.abs(fm)[ifm]
  145. # Find rate of change of frequency at the center of the pulse
  146. if dfm == 0:
  147. pm0 = pm[ifm]
  148. am0 = am[ifm]
  149. roc_fm0 = np.abs(fm[ifm + 1] - fm[ifm - 1]) / 2 / dwell
  150. else: # We need to bracket the zero-crossing
  151. if fm[ifm] * fm[ifm + 1] < 0:
  152. b = 1
  153. else:
  154. b = -1
  155. pm0 = (pm[ifm] * fm[ifm + b] - pm[ifm + b] * fm[ifm]) / (fm[ifm + b] - fm[ifm])
  156. am0 = (am[ifm] * fm[ifm + b] - am[ifm + b] * fm[ifm]) / (fm[ifm + b] - fm[ifm])
  157. roc_fm0 = np.abs(fm[ifm] - fm[ifm + b]) / dwell
  158. pm -= pm0
  159. a = (roc_fm0 * adiabaticity) ** 0.5 / 2 / np.pi / am0
  160. signal = a * am * np.exp(1j * pm)
  161. if N != n_raw:
  162. n_pad = n_raw - N
  163. signal = [
  164. np.zeros(1, n_pad - np.floor(n_pad / 2)),
  165. signal,
  166. np.zeros(1, np.floor(n_pad / 2)),
  167. ]
  168. N = n_raw
  169. t = (np.arange(1, N + 1) - 0.5) * dwell
  170. rf = SimpleNamespace()
  171. rf.type = "rf"
  172. rf.signal = signal
  173. rf.t = t
  174. rf.shape_dur = N * dwell
  175. rf.freq_offset = freq_offset
  176. rf.phase_offset = phase_offset
  177. rf.dead_time = system.rf_dead_time
  178. rf.ringdown_time = system.rf_ringdown_time
  179. rf.delay = delay
  180. if use != "":
  181. rf.use = use
  182. else:
  183. rf.use = "inversion"
  184. if rf.dead_time > rf.delay:
  185. rf.delay = rf.dead_time
  186. if return_gz:
  187. if slice_thickness <= 0:
  188. raise ValueError("Slice thickness must be provided")
  189. if max_grad > 0:
  190. system.max_grad = max_grad
  191. if max_slew > 0:
  192. system.max_slew = max_slew
  193. if pulse_type == "hypsec":
  194. bandwidth = mu * beta / np.pi
  195. elif pulse_type == "wurst":
  196. bandwidth = bandwidth
  197. else:
  198. raise ValueError("Unsupported adiabatic pulse type.")
  199. center_pos, _ = calc_rf_center(rf)
  200. amplitude = bandwidth / slice_thickness
  201. area = amplitude * duration
  202. gz = make_trapezoid(
  203. channel="z", system=system, flat_time=duration, flat_area=area
  204. )
  205. gzr = make_trapezoid(
  206. channel="z",
  207. system=system,
  208. area=-area * (1 - center_pos) - 0.5 * (gz.area - area),
  209. )
  210. if rf.delay > gz.rise_time: # Round-up to gradient raster
  211. gz.delay = (
  212. np.ceil((rf.delay - gz.rise_time) / system.grad_raster_time)
  213. * system.grad_raster_time
  214. )
  215. if rf.delay < (gz.rise_time + gz.delay):
  216. rf.delay = gz.rise_time + gz.delay
  217. if rf.ringdown_time > 0 and return_delay:
  218. delay = make_delay(calc_duration(rf) + rf.ringdown_time)
  219. if return_gz and return_delay:
  220. return rf, gz, gzr, delay
  221. elif return_gz:
  222. return rf, gz, gzr
  223. else:
  224. return rf