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 LF_scanner.pypulseq import eps
  6. from LF_scanner.pypulseq.calc_duration import calc_duration
  7. from LF_scanner.pypulseq.calc_rf_center import calc_rf_center
  8. from LF_scanner.pypulseq.make_delay import make_delay
  9. from LF_scanner.pypulseq.make_trapezoid import make_trapezoid
  10. from LF_scanner.pypulseq.opts import Opts
  11. from LF_scanner.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