SAR_calc.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. # Copyright of the Board of Trustees of Columbia University in the City of New York
  2. from pathlib import Path
  3. from typing import Tuple
  4. from typing import Union
  5. import matplotlib.pyplot as plt
  6. import numpy as np
  7. import numpy.matlib
  8. import scipy.io as sio
  9. from scipy import interpolate
  10. from seqgen.pypulseq.Sequence.sequence import Sequence
  11. from seqgen.pypulseq.calc_duration import calc_duration
  12. def _calc_SAR(Q: np.ndarray, I: np.ndarray) -> np.ndarray:
  13. """
  14. Compute the SAR output for a given Q matrix and I current values.
  15. Parameters
  16. ----------
  17. Q : numpy.ndarray
  18. Q matrix. Refer Graesslin, Ingmar, et al. "A specific absorption rate prediction concept for parallel
  19. transmission MR." Magnetic resonance in medicine 68.5 (2012): 1664-1674.
  20. I : numpy.ndarray
  21. I matrix, capturing the current (in Amps) on each of the transmit channels. Refer Graesslin, Ingmar, et al. "A
  22. specific absorption rate prediction concept for parallel transmission MR." Magnetic resonance in medicine
  23. 68.5 (2012): 1664-1674.
  24. Returns
  25. -------
  26. SAR : numpy.ndarray
  27. Contains the SAR value for a particular Q matrix
  28. """
  29. if len(I.shape) == 1: # Just to fit the multi-transmit case for now, TODO
  30. I = np.tile(I, (Q.shape[0], 1)) # Nc x Nt
  31. I_fact = np.divide(np.matmul(I, np.conjugate(I).T), I.shape[1])
  32. SAR_temp = np.multiply(Q, I_fact)
  33. SAR = np.abs(np.sum(SAR_temp[:]))
  34. return SAR
  35. def _load_Q() -> Tuple[np.ndarray, np.ndarray]:
  36. """
  37. Load Q matrix that is precomputed based on the VHM model for 8 channels. Refer Graesslin, Ingmar, et al. "A
  38. specific absorption rate prediction concept for parallel transmission MR." Magnetic resonance in medicine 68.5
  39. (2012): 1664-1674.
  40. Returns
  41. -------
  42. Qtmf, Qhmf : numpy.ndarray
  43. Contains the Q-matrix of global SAR values for body-mass and head-mass respectively.
  44. """
  45. # Load relevant Q matrices computed from the model - this code will be integrated later - starting from E fields
  46. path_Q = str(Path(__file__).parent / "QGlobal.mat")
  47. Q = sio.loadmat(path_Q)
  48. Q = Q["Q"]
  49. val = Q[0, 0]
  50. Qtmf = val["Qtmf"]
  51. Qhmf = val["Qhmf"]
  52. return Qtmf, Qhmf
  53. def _SAR_from_seq(
  54. seq: Sequence, Qtmf: np.ndarray, Qhmf: np.ndarray
  55. ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
  56. """
  57. Compute global whole body and head only SAR values for the given `seq` object.
  58. Parameters
  59. ----------
  60. seq : Sequence
  61. Sequence object to calculate for which SAR values will be calculated.
  62. Qtmf : numpy.ndarray
  63. Q-matrix of global SAR values for body-mass.
  64. Qhmf : numpy.ndarray
  65. Q-matrix of global SAR values for head-mass.
  66. Returns
  67. -------
  68. SAR_wbg : numpy.ndarray
  69. SAR values for body-mass.
  70. SAR_hg : numpy.ndarray
  71. SAR values for head-mass.
  72. t : numpy.ndarray
  73. Corresponding time points.
  74. """
  75. # Identify RF blocks and compute SAR - 10 seconds must be less than twice and 6 minutes must be less than
  76. # 4 (WB) and 3.2 (head-20)
  77. block_events = seq.block_events
  78. num_events = len(block_events)
  79. t = np.zeros(num_events)
  80. SAR_wbg = np.zeros(t.shape)
  81. SAR_hg = np.zeros(t.shape)
  82. t_prev = 0
  83. for block_counter in block_events:
  84. block = seq.get_block(block_counter)
  85. block_dur = calc_duration(block)
  86. t[block_counter - 1] = t_prev + block_dur
  87. t_prev = t[block_counter - 1]
  88. if hasattr(block, "rf"): # has rf
  89. rf = block.rf
  90. signal = rf.signal
  91. # This rf could be parallel transmit as well
  92. SAR_wbg[block_counter] = _calc_SAR(Qtmf, signal)
  93. SAR_hg[block_counter] = _calc_SAR(Qhmf, signal)
  94. return SAR_wbg, SAR_hg, t
  95. def _SAR_interp(SAR: np.ndarray, t: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
  96. """
  97. Interpolate SAR values for one second resolution.
  98. Parameters
  99. ----------
  100. SAR : numpy.ndarray
  101. SAR values
  102. t : numpy.ndarray
  103. Current time points.
  104. Returns
  105. -------
  106. SAR_interp : numpy.ndarray
  107. Interpolated values of SAR for a temporal resolution of 1 second.
  108. t_sec : numpy.ndarray
  109. Time points at 1 second resolution.
  110. """
  111. t_sec = np.arange(1, np.floor(t[-1]) + 1, 1)
  112. f = interpolate.interp1d(t, SAR)
  113. SAR_interp = f(t_sec)
  114. return SAR_interp, t_sec
  115. def _SAR_lims_check(
  116. SARwbg_lim_s, SARhg_lim_s, tsec
  117. ) -> Tuple[
  118. np.ndarray,
  119. np.ndarray,
  120. np.ndarray,
  121. np.ndarray,
  122. np.ndarray,
  123. np.ndarray,
  124. np.ndarray,
  125. np.ndarray,
  126. ]:
  127. """
  128. Check for SAR violations as compared to IEC 10 second and 6 minute averages;
  129. returns SAR values that are interpolated for the fixed IEC time intervals.
  130. Parameters
  131. ----------
  132. SARwbg_lim_s : numpy.ndarray
  133. SARhg_lim_s : numpy.ndarray
  134. tsec : numpy.ndarray
  135. Returns
  136. -------
  137. SAR_wbg_tensec : numpy.ndarray
  138. SAR_wbg_sixmin : numpy.ndarray
  139. SAR_hg_tensec : numpy.ndarray
  140. SAR_hg_sixmin : numpy.ndarray
  141. SAR_wbg_sixmin_peak : numpy.ndarray
  142. SAR_hg_sixmin_peak : numpy.ndarray
  143. SAR_wbg_tensec_peak : numpy.ndarray
  144. SAR_hg_tensec_peak : numpy.ndarray
  145. """
  146. if tsec[-1] > 10:
  147. six_min_threshold_wbg = 4
  148. ten_sec_threshold_wbg = 8
  149. six_min_threshold_hg = 3.2
  150. ten_sec_threshold_hg = 6.4
  151. SAR_wbg_lim_app = np.concatenate(
  152. (np.zeros(5), SARwbg_lim_s, np.zeros(5)), axis=0
  153. )
  154. SAR_hg_lim_app = np.concatenate((np.zeros(5), SARhg_lim_s, np.zeros(5)), axis=0)
  155. SAR_wbg_tensec = _do_sw_sar(SAR_wbg_lim_app, tsec, 10) # < 2 SARmax
  156. SAR_hg_tensec = _do_sw_sar(SAR_hg_lim_app, tsec, 10) # < 2 SARmax
  157. SAR_wbg_tensec_peak = np.round(np.max(SAR_wbg_tensec), 2)
  158. SAR_hg_tensec_peak = np.round(np.max(SAR_hg_tensec), 2)
  159. if (np.max(SAR_wbg_tensec) > ten_sec_threshold_wbg) or (
  160. np.max(SAR_hg_tensec) > ten_sec_threshold_hg
  161. ):
  162. print("Pulse exceeding 10 second Global SAR limits, increase TR")
  163. SAR_wbg_sixmin = "NA"
  164. SAR_hg_sixmin = "NA"
  165. SAR_wbg_sixmin_peak = "NA"
  166. SAR_hg_sixmin_peak = "NA"
  167. if tsec[-1] > 600:
  168. SAR_wbg_lim_app = np.concatenate(
  169. (np.zeros(300), SARwbg_lim_s, np.zeros(300)), axis=0
  170. )
  171. SAR_hg_lim_app = np.concatenate(
  172. (np.zeros(300), SARhg_lim_s, np.zeros(300)), axis=0
  173. )
  174. SAR_hg_sixmin = _do_sw_sar(SAR_hg_lim_app, tsec, 600)
  175. SAR_wbg_sixmin = _do_sw_sar(SAR_wbg_lim_app, tsec, 600)
  176. SAR_wbg_sixmin_peak = np.round(np.max(SAR_wbg_sixmin), 2)
  177. SAR_hg_sixmin_peak = np.round(np.max(SAR_hg_sixmin), 2)
  178. if (np.max(SAR_hg_sixmin) > six_min_threshold_wbg) or (
  179. np.max(SAR_hg_sixmin) > six_min_threshold_hg
  180. ):
  181. print("Pulse exceeding 10 second Global SAR limits, increase TR")
  182. else:
  183. print("Need at least 10 seconds worth of sequence to calculate SAR")
  184. SAR_wbg_tensec = "NA"
  185. SAR_wbg_sixmin = "NA"
  186. SAR_hg_tensec = "NA"
  187. SAR_hg_sixmin = "NA"
  188. SAR_wbg_sixmin_peak = "NA"
  189. SAR_hg_sixmin_peak = "NA"
  190. SAR_wbg_tensec_peak = "NA"
  191. SAR_hg_tensec_peak = "NA"
  192. return (
  193. SAR_wbg_tensec,
  194. SAR_wbg_sixmin,
  195. SAR_hg_tensec,
  196. SAR_hg_sixmin,
  197. SAR_wbg_sixmin_peak,
  198. SAR_hg_sixmin_peak,
  199. SAR_wbg_tensec_peak,
  200. SAR_hg_tensec_peak,
  201. )
  202. def _do_sw_sar(SAR: np.ndarray, tsec: np.ndarray, t: np.ndarray) -> np.ndarray:
  203. """
  204. Compute a sliding window average of SAR values.
  205. Parameters
  206. ----------
  207. SAR : numpy.ndarray
  208. SAR values.
  209. tsec : numpy.ndarray
  210. Corresponding time points at 1 second resolution.
  211. t : numpy.ndarray
  212. Corresponding time points.
  213. Returns
  214. -------
  215. SAR_timeavag : numpy.ndarray
  216. Sliding window time average of SAR values.
  217. """
  218. SAR_time_avg = np.zeros(len(tsec) + int(t))
  219. for instant in range(
  220. int(t / 2), int(t / 2) + (int(tsec[-1]))
  221. ): # better to go from -sw / 2: sw / 2
  222. SAR_time_avg[instant] = (
  223. sum(SAR[range(instant - int(t / 2), instant + int(t / 2) - 1)]) / t
  224. )
  225. SAR_time_avg = SAR_time_avg[int(t / 2) : int(t / 2) + (int(tsec[-1]))]
  226. return SAR_time_avg
  227. def calc_SAR(file: Union[str, Path, Sequence]) -> None:
  228. """
  229. Compute Global SAR values on the `.seq` object for head and whole body over the specified time averages.
  230. Parameters
  231. ----------
  232. file : str, Path or Seuqence
  233. `.seq` file for which global SAR values will be computed. Can be path to `.seq` file as `str` or `Path`, or the
  234. `Sequence` object itself.
  235. Raises
  236. ------
  237. ValueError
  238. If `file` is a `str` or `Path` to the `.seq` file and this file does not exist on disk.
  239. """
  240. if isinstance(file, (str, Path)):
  241. if isinstance(file, str):
  242. file = Path(file)
  243. if file.exists() and file.is_file():
  244. seq_obj = Sequence()
  245. seq_obj.read(str(file))
  246. seq_obj = seq_obj
  247. else:
  248. raise ValueError("Seq file does not exist.")
  249. else:
  250. seq_obj = file
  251. Q_tmf, Q_hmf = _load_Q()
  252. SAR_wbg, SAR_hg, t = _SAR_from_seq(seq_obj, Q_tmf, Q_hmf)
  253. SARwbg_lim, tsec = _SAR_interp(SAR_wbg, t)
  254. SARhg_lim, tsec = _SAR_interp(SAR_hg, t)
  255. (
  256. SAR_wbg_tensec,
  257. SAR_wbg_sixmin,
  258. SAR_hg_tensec,
  259. SAR_hg_sixmin,
  260. SAR_wbg_sixmin_peak,
  261. SAR_hg_sixmin_peak,
  262. SAR_wbg_tensec_peak,
  263. SAR_hg_tensec_peak,
  264. ) = _SAR_lims_check(SARwbg_lim, SARhg_lim, tsec)
  265. # Plot 10 sec average SAR
  266. if tsec[-1] > 10:
  267. plt.plot(tsec, SAR_wbg_tensec, "x-", label="Whole Body: 10sec")
  268. plt.plot(tsec, SAR_hg_tensec, ".-", label="Head only: 10sec")
  269. # plt.plot(t, SARwbg, label='Whole Body - instant')
  270. # plt.plot(t, SARhg, label='Whole Body - instant')
  271. plt.xlabel("Time (s)")
  272. plt.ylabel("SAR (W/kg)")
  273. plt.title("Global SAR - Mass Normalized - Whole body and head only")
  274. plt.legend()
  275. plt.grid(True)
  276. plt.show()