ext_test_report.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import numpy as np
  2. from seqgen.pypulseq import eps
  3. from seqgen.pypulseq.convert import convert
  4. def ext_test_report(self) -> str:
  5. """
  6. Analyze the sequence and return a text report.
  7. Returns
  8. -------
  9. report : str
  10. """
  11. # Find RF pulses and list flip angles
  12. flip_angles_deg = []
  13. for k in self.rf_library.keys:
  14. lib_data = self.rf_library.data[k]
  15. if len(self.rf_library.type) >= k:
  16. rf = self.rf_from_lib_data(lib_data, self.rf_library.type[k])
  17. else:
  18. rf = self.rf_from_lib_data(lib_data)
  19. flip_angles_deg.append(
  20. np.abs(np.sum(rf.signal[:-1] * (rf.t[1:] - rf.t[:-1]))) * 360
  21. )
  22. flip_angles_deg = np.unique(flip_angles_deg)
  23. # Calculate TE, TR
  24. duration, num_blocks, event_count = self.duration()
  25. k_traj_adc, k_traj, t_excitation, t_refocusing, t_adc = self.calculate_kspacePP()
  26. k_abs_adc = np.sqrt(np.sum(np.square(k_traj_adc), axis=0))
  27. k_abs_echo, index_echo = np.min(k_abs_adc), np.argmin(k_abs_adc)
  28. t_echo = t_adc[index_echo]
  29. if k_abs_echo > eps:
  30. i2check = []
  31. # Check if ADC k-space trajectory has elements left and right to index_echo
  32. if index_echo > 1:
  33. i2check.append(index_echo - 1)
  34. if index_echo < len(k_abs_adc):
  35. i2check.append(index_echo + 1)
  36. for a in range(len(i2check)):
  37. v_i_to_0 = k_traj_adc[:, index_echo]
  38. v_i_to_t = k_traj_adc[:, i2check[a]] - k_traj_adc[:, index_echo]
  39. # Project v_i_to_0 to v_i_to_t
  40. p_vit = np.matmul(v_i_to_0, v_i_to_t) / np.square(np.linalg.norm(v_i_to_t))
  41. if p_vit > 0:
  42. # We have found a bracket for the echo and the proportionality coefficient is p_vit
  43. t_echo = t_adc[index_echo] * (1 - p_vit) + t_adc[i2check[a]] * p_vit
  44. if len(t_excitation) != 0:
  45. t_ex_tmp = t_excitation[t_excitation < t_echo]
  46. TE = t_echo - t_ex_tmp[-1]
  47. else:
  48. TE = np.nan
  49. if len(t_excitation) < 2:
  50. TR = duration
  51. else:
  52. t_ex_tmp1 = t_excitation[t_excitation > t_echo]
  53. if len(t_ex_tmp1) == 0:
  54. TR = t_ex_tmp[-1] - t_ex_tmp[-2]
  55. else:
  56. TR = t_ex_tmp1[0] - t_ex_tmp[-1]
  57. # Check sequence dimensionality and spatial resolution
  58. k_extent = np.max(np.abs(k_traj_adc), axis=1)
  59. k_scale = np.max(k_extent)
  60. is_cartesian = False
  61. if k_scale != 0:
  62. k_bins = 4e6
  63. k_threshold = k_scale / k_bins
  64. # Detect unused dimensions and delete them
  65. if np.any(k_extent < k_threshold):
  66. k_traj_adc = np.delete(k_traj_adc, np.where(k_extent < k_threshold), axis=0)
  67. k_extent = np.delete(k_extent, np.where(k_extent < k_threshold), axis=0)
  68. # Bin the k-space trajectory to detect repetitions / slices
  69. k_len = k_traj_adc.shape[1]
  70. k_repeat = np.zeros(k_len)
  71. k_storage = np.zeros(k_len)
  72. k_storage_next = 0
  73. k_map = dict()
  74. for i in range(k_len):
  75. key_string = str(
  76. (k_bins + np.round(k_traj_adc[:, i] / k_threshold)).astype(np.int32)
  77. )
  78. k_storage_ind = k_map.get(key_string)
  79. if k_storage_ind is None:
  80. k_storage_ind = k_storage_next
  81. k_map[key_string] = k_storage_ind
  82. k_storage_next += 1
  83. k_storage[k_storage_ind] = k_storage[k_storage_ind] + 1
  84. k_repeat[i] = k_storage[k_storage_ind]
  85. repeats_max = np.max(k_storage[:k_storage_next])
  86. repeats_min = np.min(k_storage[:k_storage_next])
  87. repeats_median = np.median(k_storage[:k_storage_next])
  88. repeats_unique = np.unique(k_storage[:k_storage_next])
  89. counts_unique = np.zeros_like(repeats_unique)
  90. for i in range(len(repeats_unique)):
  91. counts_unique[i] = np.sum(
  92. repeats_unique[i] == k_storage[: k_storage_next - 1]
  93. )
  94. k_traj_rep1 = k_traj_adc[:, k_repeat == 1]
  95. k_counters = np.zeros_like(k_traj_rep1)
  96. dims = k_traj_rep1.shape[0]
  97. k_map = dict()
  98. for j in range(dims):
  99. k_storage = np.zeros(k_len)
  100. k_storage_next = 1
  101. for i in range(k_traj_rep1.shape[1]):
  102. key = np.round(k_traj_rep1[j, i] / k_threshold).astype(np.int32)
  103. k_storage_ind = k_map.get(key)
  104. if k_storage_ind is None:
  105. k_storage_ind = k_map.get(key + 1)
  106. if k_storage_ind is None:
  107. k_storage_ind = k_map.get(key - 1)
  108. if k_storage_ind is None:
  109. k_storage_ind = k_storage_next
  110. k_map[key] = k_storage_ind
  111. k_storage_next += 1
  112. k_storage[k_storage_ind] = k_traj_rep1[j, i]
  113. k_counters[j, i] = k_storage_ind
  114. unique_k_positions = np.max(k_counters, axis=1)
  115. is_cartesian = np.prod(unique_k_positions) == k_traj_rep1.shape[1]
  116. else:
  117. unique_k_positions = 1
  118. # gw_data = self.gradient_waveforms()
  119. waveforms_and_times = self.waveforms_and_times()
  120. gw_data = waveforms_and_times[0]
  121. gws = np.zeros_like(gw_data)
  122. ga = np.zeros(len(gw_data))
  123. gs = np.zeros(len(gw_data))
  124. common_time = np.unique(np.concatenate(gw_data, axis=1)[0])
  125. gw_ct = np.zeros((len(gw_data), len(common_time)))
  126. gs_ct = np.zeros((len(gw_data), len(common_time) - 1))
  127. for gc in range(len(gw_data)):
  128. if gw_data[gc].shape[1] > 0:
  129. # Slew
  130. gws[gc] = (gw_data[gc][1, 1:] - gw_data[gc][1, :-1]) / (
  131. gw_data[gc][0, 1:] - gw_data[gc][0, :-1]
  132. )
  133. # Interpolate to common time
  134. gw_ct[gc] = np.interp(
  135. x=common_time,
  136. xp=gw_data[gc][0, :],
  137. fp=gw_data[gc][1, :],
  138. left=0,
  139. right=0,
  140. )
  141. gs_ct[gc] = (gw_ct[gc][1:] - gw_ct[gc][:-1]) / (
  142. common_time[1:] - common_time[:-1]
  143. )
  144. # Max grad/slew per channel
  145. ga[gc] = np.max(np.abs(gw_data[gc][1:]))
  146. gs[gc] = np.max(np.abs(gws[gc]))
  147. ga_abs = np.max(np.sqrt(np.sum(np.square(gw_ct), axis=0)))
  148. gs_abs = np.max(np.sqrt(np.sum(np.square(gs_ct), axis=0)))
  149. timing_ok, timing_error_report = self.check_timing()
  150. report = (
  151. f"Number of blocks: {num_blocks}\n"
  152. f"Number of events:\n"
  153. f"RF: {event_count[1]:6.0f}\n"
  154. f"Gx: {event_count[2]:6.0f}\n"
  155. f"Gy: {event_count[3]:6.0f}\n"
  156. f"Gz: {event_count[4]:6.0f}\n"
  157. f"ADC: {event_count[5]:6.0f}\n"
  158. f"Delay: {event_count[0]:6.0f}\n"
  159. f"Sequence duration: {duration:.6f} s\n"
  160. f"TE: {TE:.6f} s\n"
  161. f"TR: {TR:.6f} s\n"
  162. )
  163. report += (
  164. "Flip angle: "
  165. + ("{:.02f} " * len(flip_angles_deg)).format(*flip_angles_deg)
  166. + "deg\n"
  167. )
  168. report += (
  169. "Unique k-space positions (aka cols, rows, etc.): "
  170. + ("{:.0f} " * len(unique_k_positions)).format(*unique_k_positions)
  171. + "\n"
  172. )
  173. if np.any(unique_k_positions > 1):
  174. report += f"Dimensions: {len(k_extent)}\n"
  175. report += ("Spatial resolution: {:.02f} mm\n" * len(k_extent)).format(
  176. *(0.5 / k_extent * 1e3)
  177. )
  178. report += f"Repetitions/slices/contrasts: {repeats_median}; range: [{repeats_min, repeats_max}]\n"
  179. if is_cartesian:
  180. report += "Cartesian encoding trajectory detected\n"
  181. else:
  182. report += "Non-cartesian/irregular encoding trajectory detected (eg: EPI, spiral, radial, etc.)\n"
  183. if timing_ok:
  184. report += "Event timing check passed successfully\n"
  185. else:
  186. report += (
  187. f"Event timing check failed. Error listing follows:\n {timing_error_report}"
  188. )
  189. ga_converted = convert(from_value=ga, from_unit="Hz/m", to_unit="mT/m")
  190. gs_converted = convert(from_value=gs, from_unit="Hz/m/s", to_unit="T/m/s")
  191. report += (
  192. "Max gradient: "
  193. + ("{:.0f} " * len(ga)).format(*ga)
  194. + "Hz/m == "
  195. + ("{:.02f} " * len(ga_converted)).format(*ga_converted)
  196. + "mT/m\n"
  197. )
  198. report += (
  199. "Max slew rate: "
  200. + ("{:.0f} " * len(gs)).format(*gs)
  201. + "Hz/m/s == "
  202. + ("{:.02f} " * len(ga_converted)).format(*gs_converted)
  203. + "T/m/s\n"
  204. )
  205. ga_abs_converted = convert(from_value=ga_abs, from_unit="Hz/m", to_unit="mT/m")
  206. gs_abs_converted = convert(from_value=gs_abs, from_unit="Hz/m/s", to_unit="T/m/s")
  207. report += (
  208. f"Max absolute gradient: {ga_abs:.0f} Hz/m == {ga_abs_converted:.2f} mT/m\n"
  209. )
  210. report += (
  211. f"Max absolute slew rate: {gs_abs:g} Hz/m/s == {gs_abs_converted:.2f} T/m/s"
  212. )
  213. return report